Skip to content

fix(devin): support ATIF v1.7 transcripts#526

Draft
bmcdonough wants to merge 5 commits into
getagentseal:mainfrom
bmcdonough:fix/devin-atif-v17
Draft

fix(devin): support ATIF v1.7 transcripts#526
bmcdonough wants to merge 5 commits into
getagentseal:mainfrom
bmcdonough:fix/devin-atif-v17

Conversation

@bmcdonough

@bmcdonough bmcdonough commented Jun 19, 2026

Copy link
Copy Markdown

What

  • update the Devin provider parser to handle ATIF v1.7 transcript shape (//step-level + )
  • keep compatibility with existing Devin transcript fields by normalizing legacy and newer cost/token fields
  • add coverage for ATIF v1.5, v1.6, and v1.7 transcript variants
  • refresh Devin provider docs for supported ATIF versions and field mappings

Why

Recent Devin CLI transcripts use newer ATIF shapes (including v1.7) that are not strictly metadata-scoped. This change ensures codeburn can ingest these sessions end-to-end while preserving existing behavior.

Testing-

codeburn@0.9.12 test
vitest tests/providers/devin.test.ts

RUN v3.2.6 /Users/william.mcdonough/github/bmcdonough/codeburn

✓ tests/providers/devin.test.ts (14 tests) 60ms

Test Files 1 passed (1)
Tests 14 passed (14)
Start at 15:18:26
Duration 600ms (transform 180ms, setup 0ms, collect 244ms, tests 60ms, environment 0ms, prepare 111ms)

Comment thread docs/providers/devin.md
Comment thread docs/providers/devin.md Outdated

User-input steps (`metadata.is_user_input === true`) are skipped. Non-user
steps are included only if they have positive ACU usage or positive token usage.
CodeBurn supports ATIF transcript variants used by Devin across **ATIF-v1.5**, **ATIF-v1.6**, and **ATIF-v1.7** (and remains backward-compatible with older Devin transcripts that still use v1.4-style field names).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is I would not frame as "CodeBurn supports ATIF". The existing code is tied to devin. For that claim to be true. The code would need to be on a separate place and be shared by other providers, which at this point is not.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the wording, let me know your thoughts.

Comment thread docs/providers/devin.md Outdated
Comment thread docs/providers/devin.md

`metadata.committed_acu_cost` is per step, not cumulative. The provider converts
each step with:
`costUSD` is always provider-supplied and uses configured ACU conversion:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still per step and not cumulative, right?

Comment thread docs/providers/devin.md
using Devin's current export convention:

```text
committed_acu_cost = committed_credit_cost / 10000

@tvcsantos tvcsantos Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the source of this? Can you provide a reference?

Also IMO this should not be hardcoded, but rather configurable. Because Devin can change it at any time, and most likely on enterprise accounts can even depend on the agreed plan.

Comment thread docs/providers/devin.md
`devin:<sessionId>:<step.step_id>`

The provider name is part of the key via the `devin:` prefix.
When `step_id` is missing, parser falls back to 1-based step index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step_id accoding to ATIF RFC has never been optional and is always mandatory. So it will never be missing

Comment thread src/providers/devin.ts Outdated
Comment thread src/providers/devin.ts Outdated
Comment thread src/providers/devin.ts Outdated
Comment thread src/providers/devin.ts Outdated
Comment thread src/providers/devin.ts Outdated
Comment thread src/providers/devin.ts Outdated
name?: string;
version?: string;
model_name?: string;
tool_definitions?: unknown;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not unknown, but rather an array following openai functional calling schema

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set name, version to required.
set tool_definitions to array.

Comment thread src/providers/devin.ts
type ToolCall = {
tool_call_id: string;
function_name: string;
tool_call_id?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field is mandatory, not optional

Comment thread src/providers/devin.ts
tool_call_id: string;
function_name: string;
tool_call_id?: string;
function_name?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field is mandatory, not optional

Comment thread src/providers/devin.ts
function_name: string;
tool_call_id?: string;
function_name?: string;
function?: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not exist. only exists on tool definition

Comment thread src/providers/devin.ts
type Step = {
step_id: number;
source: string;
step_id?: number | string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step_id is a number according to ATIF RFC

Comment thread src/providers/devin.ts
step_id: number;
source: string;
step_id?: number | string;
source?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source is required and not optional

Comment thread src/providers/devin.ts
timestamp?: string;
model_name?: string;
message: string;
message?: unknown;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message is mandatory and not optional.

The type if we want to accept versions above 1.6+ should be array or a string.

When is an array then it has the following schema

{ type: string, text?: string, source?: unknown }

Comment thread src/providers/devin.ts
model_name?: string;
message: string;
message?: unknown;
metrics?: DevinMetrics;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be tied to DevinMetrics. Step type is supposed to be generic and unrelated with Devin.

Comment thread src/providers/devin.ts
message: string;
message?: unknown;
metrics?: DevinMetrics;
metadata?: DevinMetadata;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metadata is not part of ATIF at step level and this seems to be custom to devin. Hence should not be declared in this type.

Comment thread src/providers/devin.ts
message?: unknown;
metrics?: DevinMetrics;
metadata?: DevinMetadata;
extra?: DevinStepExtra;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra is part of the spec but is an object with no bound contract.

Since Step type is supposed to be generic for ATIF it should not be tied to devin in any way

Comment thread src/providers/devin.ts
};

type DevinStep = Step & { metadata?: DevinMetadata };
type DevinStep = Step;

@tvcsantos tvcsantos Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preivous version was intented since this type was defined to provide the custom DevinStep that extends Step with custom attributes and information

Comment thread src/providers/devin.ts
const DEVIN_PROVIDER_DISPLAY_NAME = "Devin";
const DEVIN_TRANSCRIPTS_SUBDIR = "transcripts";
const DEVIN_SESSIONS_DB = "sessions.db";
const DEVIN_CREDITS_PER_ACU = 10_000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the source for this? IMO this should be configurable, since Devin can change, and also might depend on subscription plan

Comment thread src/providers/devin.ts
Comment on lines -102 to +131
return JSON.parse(raw) as DevinAgentTrajectory;
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return null;
}
return parsed as DevinAgentTrajectory;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the complex defensive programming. The transcript must be allways from ATIF format

Comment thread src/providers/devin.ts
Comment on lines +309 to +315
const tools: string[] = [];
const toolCalls = Array.isArray(step.tool_calls) ? step.tool_calls : [];
for (const call of toolCalls) {
const toolName = call.function_name ?? call.function?.name;
if (toolName) tools.push(toolName);
}
return tools;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. tool_calls is always an array
  2. function.name is not part of the ATIF schema for tool_calls. only function_name is the valid attribute. So no need for the extra logic

Comment thread src/providers/devin.ts

const transcript = parseTranscript(raw);
if (!transcript?.steps) return;
if (!transcript?.steps || !Array.isArray(transcript.steps)) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

steps is always an array so no need for the check

Comment thread src/providers/devin.ts
Comment on lines +400 to +401
if (!step || typeof step !== "object" || Array.isArray(step)) continue;
if (isUserInputStep(step)) continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step is always defined and is an object with several mandatory fields. So no need for the check. It can be simplified to

Suggested change
if (!step || typeof step !== "object" || Array.isArray(step)) continue;
if (isUserInputStep(step)) continue;
if (isUserInputStep(step)) continue;

Comment thread src/providers/devin.ts
const timestamp = getTimestamp(step, session) ?? "";

const deduplicationKey = `devin:${sessionId}:${step.step_id}`;
const stepId = `${step.step_id ?? index + 1}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step id is mandatory so no need for this

Comment thread src/providers/devin.ts
Comment on lines +167 to +183
const metadataAcuCost = safeNumber(step.metadata?.committed_acu_cost);
if (isPositiveNumber(metadataAcuCost)) return metadataAcuCost;

const extraAcuCost = safeNumber(step.extra?.committed_acu_cost);
if (isPositiveNumber(extraAcuCost)) return extraAcuCost;

const metadataCreditCost = safeNumber(step.metadata?.committed_credit_cost);
if (isPositiveNumber(metadataCreditCost)) {
return metadataCreditCost / DEVIN_CREDITS_PER_ACU;
}

const extraCreditCost = safeNumber(step.extra?.committed_credit_cost);
if (isPositiveNumber(extraCreditCost)) {
return extraCreditCost / DEVIN_CREDITS_PER_ACU;
}

return 0;

@tvcsantos tvcsantos Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic can be grouped in two blocks. ACU costs and Credit costs. The computational effort vs readability in my opinion does not pay all the repetition for the early returns. Also isPositiveNumber subsumes safeNumber.

Suggested change
const metadataAcuCost = safeNumber(step.metadata?.committed_acu_cost);
if (isPositiveNumber(metadataAcuCost)) return metadataAcuCost;
const extraAcuCost = safeNumber(step.extra?.committed_acu_cost);
if (isPositiveNumber(extraAcuCost)) return extraAcuCost;
const metadataCreditCost = safeNumber(step.metadata?.committed_credit_cost);
if (isPositiveNumber(metadataCreditCost)) {
return metadataCreditCost / DEVIN_CREDITS_PER_ACU;
}
const extraCreditCost = safeNumber(step.extra?.committed_credit_cost);
if (isPositiveNumber(extraCreditCost)) {
return extraCreditCost / DEVIN_CREDITS_PER_ACU;
}
return 0;
const acuCost = [
step.metadata?.committed_acu_cost,
step.extra?.committed_acu_cost,
].filter(cost => isPositiveNumber(cost));
const creditCost = [
step.metadata?.committed_credit_cost,
step.extra?.committed_credit_cost
].filter(cost => isPositiveNumber(cost))
.map(cost => cost / DEVIN_CREDITS_PER_ACU);
return acuCost.shift() || creditCost.shift() || 0;

Comment thread src/providers/devin.ts
Comment on lines +190 to +217
const cacheCreationInputTokens = safeNumber(
metrics?.cache_creation_tokens ??
metrics?.cache_creation_input_tokens ??
metrics?.extra?.cache_creation_input_tokens,
);

const cacheReadInputTokens = safeNumber(
metrics?.cache_read_tokens ??
metrics?.cache_read_input_tokens ??
metrics?.cached_tokens ??
metrics?.extra?.cache_read_input_tokens,
);

const promptTokens = safeNumber(
metrics?.prompt_tokens ?? metrics?.total_input_tokens,
);

let inputTokens = safeNumber(metrics?.input_tokens);
if (inputTokens === 0 && promptTokens > 0) {
inputTokens = Math.max(
0,
promptTokens - cacheReadInputTokens - cacheCreationInputTokens,
);
}

const outputTokens = safeNumber(
metrics?.output_tokens ?? metrics?.completion_tokens,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to extract each one to a separate method for maintanability and also separation of concerns, making also this method getUsage easier to read.

Comment thread src/providers/devin.ts
): DevinUsage | null {
if (!metadata) return null;
const metrics = metadata.metrics;
function normalizeMessageText(message: unknown): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest first to check the schema from ATIF to avoid all this complex code. and to a proper message, taking into account ATIF schema.

@tvcsantos tvcsantos left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bmcdonough bmcdonough marked this pull request as draft June 20, 2026 16:00
@bmcdonough

Copy link
Copy Markdown
Author

converted to [DRAFT] while I work on the issues identified in the excellent review by tvcsantos

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants