From 2358e4e16a65cc30aafd1abc34cd560409088834 Mon Sep 17 00:00:00 2001 From: "ashwin.venkataraman" Date: Wed, 10 Jun 2026 12:51:40 +0530 Subject: [PATCH 1/2] update echo node --- apps/docs/docs/dialects/agentfabric/index.md | 67 +++-- dialect/agentfabric/package.json | 2 +- .../src/lint/passes/agentfabric-semantic.ts | 2 + .../src/lint/passes/rules/echo-rules.ts | 47 ++- .../passes/rules/terminal-status-rules.ts | 148 +++++++++ dialect/agentfabric/src/schema.ts | 61 +++- dialect/agentfabric/src/tests/dialect.test.ts | 6 +- dialect/agentfabric/src/tests/lint.test.ts | 283 ++++++++++++++---- ...agentfabric-customer-support-netwrok.agent | 30 +- .../agentfabric-customer-support-netwrok.yaml | 22 +- .../resources/it-help-investigation.agent | 68 ++--- .../it-help-investigation.graph.json | 62 ++-- .../resources/it-help-investigation.yaml | 20 +- .../src/tests/with-completions.test.ts | 2 +- 14 files changed, 630 insertions(+), 190 deletions(-) create mode 100644 dialect/agentfabric/src/lint/passes/rules/terminal-status-rules.ts diff --git a/apps/docs/docs/dialects/agentfabric/index.md b/apps/docs/docs/dialects/agentfabric/index.md index 56d9e357..f4c4f5d0 100644 --- a/apps/docs/docs/dialects/agentfabric/index.md +++ b/apps/docs/docs/dialects/agentfabric/index.md @@ -440,25 +440,36 @@ The router node has these properties. ### Echo Node -The echo node sends a response back to the client. The number of responses depends on the trigger interface and its configuration. Use this node for the end of a workflow, or anytime you want to emit a response. Currently only supports `a2a:response` (non-streaming). +The echo node emits an A2A event to update the stored task. It supports two event types: `a2a:status_update_event` for setting task state and messages, and `a2a:artifact_update_event` for sending artifact updates. All terminal branches must contain a status update echo that sets a terminal state. -**Example** +**Example (status update)** ```agentscript -echo a2a_response: - kind: "a2a:response" - task: a2a.task({ - state: "completed", - message: a2a.message( - { - messageId: uuid(), - parts:[ - a2a.textPart("You have been onboarded!your employee ID is" +@orchestrator.hrSystemOnboard.output.employeeId) - ] - }), - artifacts: a2a.parts(*@variables.artifacts), - metadata:None - }) +echo setStatus: + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: a2a.message({ + messageId: uuid(), + parts:[ + a2a.textPart("You have been onboarded! your employee ID is " + @orchestrator.hrSystemOnboard.output.employeeId) + ] + }) +``` + +**Example (artifact update)** + +```agentscript +echo addArtifact: + kind: "a2a:artifact_update_event" + artifact: a2a.artifact({ + artifactId: uuid(), + name: "results", + parts: [ + a2a.textPart("Analysis complete") + ] + }) + append: False + lastChunk: False ``` | Parameter | Description | Type | Required | @@ -467,16 +478,30 @@ echo a2a_response: | `label` | An optional short, human-readable display name for the node. | String | No | | `description` | A CommonMark string providing a description of the node. | String | No | | `on_exit` | A procedure that executes when the node execution finishes. | Procedure | No | -| `kind` | Discriminator for the response type. Must be "a2a:response". | String | Yes | -| `task` | A Task object as defined in the A2A specification. The `id`, `contextId` and `history` attributes are automatically populated by the trigger | Task object | Yes | +| `kind` | Event type discriminator: `"a2a:status_update_event"` or `"a2a:artifact_update_event"`. | String | Yes | +| `metadata` | Optional metadata expression for the event. | Expression | No | + +**Fields for `a2a:status_update_event`:** + +| Parameter | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `state` | A2A v1 task state (e.g. `"TASK_STATE_COMPLETED"`, `"TASK_STATE_WORKING"`, `"TASK_STATE_FAILED"`, `"TASK_STATE_CANCELED"`). | String | Yes | +| `message` | A2A message expression for the status update. | Expression | No | + +**Fields for `a2a:artifact_update_event`:** + +| Parameter | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `artifact` | A2A artifact expression via `a2a.artifact()`. | Expression | Yes | +| `append` | Whether to append to an existing artifact. Default `false`. | Boolean | No | +| `lastChunk` | Whether this is the last chunk. Default `false`. | Boolean | No | ### A2A Namespace Functions -The `a2a` namespace provides a set of functions that support A2A Task object creation. Do not prefix these functions with `@` as it's reserved for references such as `@variables`, `@actions`, `@request`, and `@orchestrator.`. +The `a2a` namespace provides a set of functions that support A2A event object creation. Do not prefix these functions with `@` as it's reserved for references such as `@variables`, `@actions`, `@request`, and `@orchestrator.`. | Function | Description | Input Arguments | Output | Example | | :---- | :---- | :---- | :---- | :---- | -| `a2a.task` | Builds an A2A Task response object | `state: str` (required) `message` (optional, from `a2a.message`) `artifacts: list` (optional, from `a2a.artifact`) `metadata: dict` (optional) | `dict` (Task) | `a2a.task("completed", message=a2a.message(...), artifacts=[a2a.artifact(...)])` | | `a2a.message` | Builds an A2A Message object | `parts: list` (required, from `a2a.textPart/a2a.dataPart/a2a.filePart`) `role: str` (optional, default: "agent") `metadata: dict` (optional) | `dict` (Message) | `a2a.message([{messageId: uuid(), parts: [a2a.textPart("Hello")]}])` | | `a2a.textPart` | Builds a TextPart object (kind: "text") | `text: str` (required) `metadata: dict` (optional) | `dict` (TextPart) | `` a2a.textPart("Employee ID: {!@orchestrator.employee.id}") `a2a.textPart("Status: Complete", metadata={priority: "high"})` `` | | `a2a.dataPart` | Builds a DataPart object (kind: "data") | `data: dict` (required) `metadata: dict` (optional) | `dict` (DataPart) | `` a2a.dataPart({employeeId: "E123", department: "Engineering"}) `a2a.dataPart(@orchestrator.result.output)` `` | @@ -486,7 +511,7 @@ The `a2a` namespace provides a set of functions that support A2A Task object cre Usage notes: -* Functions are designed to be composed: `a2a.task` accepts `messages` created by `a2a.message`, which accepts `parts` created by `a2a.textPart`/`a2a.dataPart`/`a2a.filePart`. +* Functions are designed to be composed: an echo node's `message` accepts a `a2a.message`, which accepts `parts` created by `a2a.textPart`/`a2a.dataPart`/`a2a.filePart`. * `a2a.filePart` requires either `uri` OR `bytes` (base64-encoded), but not both * `a2a.artifact` auto-generates `artifactId` if it's not provided. * Use `a2a.parts` with the `*` operator to unpack arrays, for example, `a2a.parts(*@variables.artifacts)`. diff --git a/dialect/agentfabric/package.json b/dialect/agentfabric/package.json index e91641d5..ff38fb75 100644 --- a/dialect/agentfabric/package.json +++ b/dialect/agentfabric/package.json @@ -1,6 +1,6 @@ { "name": "@agentscript/agentfabric-dialect", - "version": "0.1.30", + "version": "0.4.0", "description": "AgentFabric dialect — schema, lint rules, compiler, and dialect config", "type": "module", "main": "dist/index.js", diff --git a/dialect/agentfabric/src/lint/passes/agentfabric-semantic.ts b/dialect/agentfabric/src/lint/passes/agentfabric-semantic.ts index 0ad9ab91..6203e95c 100644 --- a/dialect/agentfabric/src/lint/passes/agentfabric-semantic.ts +++ b/dialect/agentfabric/src/lint/passes/agentfabric-semantic.ts @@ -17,6 +17,7 @@ import { checkOnExitRules } from './rules/on-exit-rules.js'; import { checkOutputStructureRules } from './rules/output-structure-rules.js'; import { checkReasoningInstructionsRules } from './rules/reasoning-instructions-rules.js'; import { checkSwitchRules } from './rules/switch-rules.js'; +import { checkTerminalStatusRules } from './rules/terminal-status-rules.js'; import { checkTriggerRules } from './rules/trigger-rules.js'; class AgentFabricSemanticPass implements LintPass { @@ -35,6 +36,7 @@ class AgentFabricSemanticPass implements LintPass { checkExecuteRules(root); checkActionBindingRules(root); checkCycleRules(root); + checkTerminalStatusRules(root); } } diff --git a/dialect/agentfabric/src/lint/passes/rules/echo-rules.ts b/dialect/agentfabric/src/lint/passes/rules/echo-rules.ts index f96fa31b..0f9ccfc9 100644 --- a/dialect/agentfabric/src/lint/passes/rules/echo-rules.ts +++ b/dialect/agentfabric/src/lint/passes/rules/echo-rules.ts @@ -7,7 +7,12 @@ import { isNamedMap } from '@agentscript/language'; import { normalizeId } from '../../utils.js'; -import { attachError, hasOwnNonNull, type AstLike } from './shared.js'; +import { A2A_TASK_STATES, A2A_TERMINAL_STATES } from '../../../schema.js'; +import { attachError, extractStringValue, type AstLike } from './shared.js'; + +const VALID_STATES = new Set(A2A_TASK_STATES); + +export { A2A_TERMINAL_STATES as TERMINAL_STATES }; export function checkEchoRules(root: Record): void { const echos = root.echo; @@ -17,15 +22,37 @@ export function checkEchoRules(root: Record): void { if (entry == null || typeof entry !== 'object') continue; const echoEntry = entry as Record; const normalizedName = normalizeId(name); - const hasTask = hasOwnNonNull(echoEntry, 'task'); - const hasMessage = hasOwnNonNull(echoEntry, 'message'); - - if (!hasTask && !hasMessage) { - attachError( - echoEntry as AstLike, - `echo '${normalizedName}' must define either 'task' or 'message'.`, - 'echo-task-or-message-required' - ); + const kind = extractStringValue(echoEntry.kind); + + if (kind === 'a2a:status_update_event') { + validateStatusUpdateEvent(echoEntry, normalizedName); + } else if (kind === 'a2a:artifact_update_event') { + validateArtifactUpdateEvent(echoEntry, normalizedName); } } } + +function validateStatusUpdateEvent( + entry: Record, + name: string +): void { + const state = extractStringValue(entry.state); + if (state !== undefined && !VALID_STATES.has(state)) { + attachError( + entry as AstLike, + `echo '${name}' has invalid state '${state}'. Valid states: ${[...VALID_STATES].join(', ')}.`, + 'echo-invalid-state' + ); + } +} + +function validateArtifactUpdateEvent( + entry: Record, + _name: string +): void { + // artifact is marked as required in the schema, so missing-field + // validation is handled by the schema layer. No additional custom + // rules needed here at this time. + void _name; + void entry; +} diff --git a/dialect/agentfabric/src/lint/passes/rules/terminal-status-rules.ts b/dialect/agentfabric/src/lint/passes/rules/terminal-status-rules.ts new file mode 100644 index 00000000..e3c5eba7 --- /dev/null +++ b/dialect/agentfabric/src/lint/passes/rules/terminal-status-rules.ts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2026, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * For full license text, see the LICENSE file in the repo root or https://www.apache.org/licenses/LICENSE-2.0 + */ + +import { isNamedMap } from '@agentscript/language'; +import { extractGraph } from '../../../graph/extractor.js'; +import { AgentFabricSchemaInfo, A2A_TERMINAL_STATES } from '../../../schema.js'; +import { attachError, extractStringValue, type AstLike } from './shared.js'; + +/** + * All terminal branches in a graph MUST contain an echo node with + * kind "a2a:status_update_event" that sets a terminal A2A state + * ("completed", "failed", or "canceled"). The echo need not be the + * leaf node — the graph may continue after it (e.g. for cleanup). + */ +export function checkTerminalStatusRules(root: Record): void { + const { nodes, edges } = extractGraph(root, AgentFabricSchemaInfo); + if (nodes.length === 0) return; + + const triggerIds = new Set( + nodes.filter(n => n.namespace === 'trigger').map(n => n.id) + ); + + const nonTriggerNodes = nodes.filter(n => !triggerIds.has(n.id)); + if (nonTriggerNodes.length === 0) return; + + // Build forward adjacency and find terminal (leaf) nodes. + const outgoingCount = new Map(); + for (const node of nonTriggerNodes) outgoingCount.set(node.id, 0); + for (const edge of edges) { + if (!outgoingCount.has(edge.from)) continue; + outgoingCount.set(edge.from, (outgoingCount.get(edge.from) ?? 0) + 1); + } + + const terminalNodeIds = nonTriggerNodes + .filter(n => (outgoingCount.get(n.id) ?? 0) === 0) + .map(n => n.id); + + if (terminalNodeIds.length === 0) return; + + // Collect the set of echo nodes that emit a terminal status update. + const terminalStatusEchoIds = collectTerminalStatusEchoIds(root); + + // If the graph already has at least one terminal status echo, check + // that every terminal node can be reached FROM one (i.e., a terminal + // status echo is an ancestor of every leaf node). + // Build reverse adjacency: for each node, which nodes point to it. + const reverseAdj = new Map(); + for (const node of nonTriggerNodes) reverseAdj.set(node.id, []); + for (const edge of edges) { + if (!reverseAdj.has(edge.to)) continue; + reverseAdj.get(edge.to)!.push(edge.from); + } + + for (const terminalId of terminalNodeIds) { + if (terminalStatusEchoIds.has(terminalId)) continue; + + if (hasAncestorInSet(terminalId, terminalStatusEchoIds, reverseAdj)) { + continue; + } + + const astNode = findAstNode(root, terminalId); + if (astNode) { + // TODO: post-GA, improve this diagnostic to show the full branch path + // and highlight which execution paths lack a terminal status update. + const shortName = terminalId.split('.').pop() ?? terminalId; + attachError( + astNode, + `Every execution path must set a terminal task state before ending. ` + + `The branch ending at '${shortName}' has no echo with kind "a2a:status_update_event" ` + + `and a terminal state (TASK_STATE_COMPLETED, TASK_STATE_FAILED, or TASK_STATE_CANCELED).`, + 'terminal-requires-status-update' + ); + } + } +} + +/** + * Collect IDs of echo nodes whose kind is "a2a:status_update_event" + * and whose state is a terminal value. + */ +function collectTerminalStatusEchoIds( + root: Record +): Set { + const ids = new Set(); + const echoEntries = root.echo; + if (!isNamedMap(echoEntries)) return ids; + + for (const [name, entry] of echoEntries) { + if (entry == null || typeof entry !== 'object') continue; + const echoEntry = entry as Record; + const kind = extractStringValue(echoEntry.kind); + if (kind !== 'a2a:status_update_event') continue; + const state = extractStringValue(echoEntry.state); + if (state !== undefined && A2A_TERMINAL_STATES.has(state)) { + ids.add(`echo.${name}`); + } + } + return ids; +} + +/** + * BFS backwards from `startId` through reverse edges to check if any + * node in `targetSet` is an ancestor. + */ +function hasAncestorInSet( + startId: string, + targetSet: Set, + reverseAdj: Map +): boolean { + const visited = new Set(); + const queue = [startId]; + visited.add(startId); + + while (queue.length > 0) { + const current = queue.shift()!; + const predecessors = reverseAdj.get(current) ?? []; + for (const pred of predecessors) { + if (targetSet.has(pred)) return true; + if (!visited.has(pred)) { + visited.add(pred); + queue.push(pred); + } + } + } + return false; +} + +function findAstNode( + root: Record, + nodeId: string +): AstLike | null { + const dotIndex = nodeId.indexOf('.'); + if (dotIndex < 0) return null; + const namespace = nodeId.slice(0, dotIndex); + const name = nodeId.slice(dotIndex + 1); + const group = root[namespace]; + if (!isNamedMap(group)) return null; + for (const [key, entry] of group as Iterable<[string, unknown]>) { + if (key === name && entry != null && typeof entry === 'object') { + return entry as AstLike; + } + } + return null; +} diff --git a/dialect/agentfabric/src/schema.ts b/dialect/agentfabric/src/schema.ts index be901d62..df79302a 100644 --- a/dialect/agentfabric/src/schema.ts +++ b/dialect/agentfabric/src/schema.ts @@ -13,6 +13,7 @@ import { SymbolKind, StringValue, NumberValue, + BooleanValue, ExpressionValue, ProcedureValue, Sequence, @@ -508,6 +509,29 @@ export const RouterBlock = NamedBlock( // ── Echo ──────────────────────────────────────────────────────────── +/** + * A2A v1 TaskState enum values (SCREAMING_SNAKE_CASE with TASK_STATE_ prefix). + * Shared across schema, compiler, and linter. + */ +export const A2A_TASK_STATES = [ + 'TASK_STATE_SUBMITTED', + 'TASK_STATE_WORKING', + 'TASK_STATE_INPUT_REQUIRED', + 'TASK_STATE_AUTH_REQUIRED', + 'TASK_STATE_COMPLETED', + 'TASK_STATE_FAILED', + 'TASK_STATE_CANCELED', + 'TASK_STATE_REJECTED', +] as const; + +/** Terminal A2A v1 task states — task lifecycle ends here. */ +export const A2A_TERMINAL_STATES = new Set([ + 'TASK_STATE_COMPLETED', + 'TASK_STATE_FAILED', + 'TASK_STATE_CANCELED', + 'TASK_STATE_REJECTED', +]); + export const EchoBlock = NamedBlock( 'EchoBlock', { @@ -516,16 +540,11 @@ export const EchoBlock = NamedBlock( 'Human-readable display name.' ).displayLabelField(), kind: StringValue.describe( - 'Response type discriminator. Currently only "a2a:response".' + 'Event type discriminator: "a2a:status_update_event" or "a2a:artifact_update_event".' ).required(), - message: ExpressionValue.describe('Message expression for the response.'), - task: ExpressionValue.describe( - 'Task expression for the A2A response (alternative to message).' + metadata: ExpressionValue.describe( + 'Optional metadata expression for the event.' ), - artifacts: ExpressionValue.describe( - 'Artifacts expression for the response.' - ), - metadata: ExpressionValue.describe('Metadata expression for the response.'), on_exit: ProcedureValue.describe( 'Procedure executed when node completes.' ).transitionContainer(), @@ -536,8 +555,29 @@ export const EchoBlock = NamedBlock( } ) .discriminant('kind') - .variant('a2a:response', {}) - .describe('Echo node that sends a response back to the client.'); + .variant('a2a:status_update_event', { + state: StringValue.describe('A2A v1 task state.') + .enum([...A2A_TASK_STATES]) + .required(), + message: ExpressionValue.describe( + 'A2A message expression for the status update.' + ), + }) + .variant('a2a:artifact_update_event', { + // TODO: ExpressionValue has no type constraint mechanism — we cannot + // enforce that this expression evaluates to an A2A Artifact object. + // Requires expression-level type inference in the language infrastructure. + artifact: ExpressionValue.describe( + 'A2A artifact expression via a2a.artifact().' + ).required(), + append: BooleanValue.describe( + 'Whether to append to an existing artifact (default: False).' + ), + lastChunk: BooleanValue.describe( + 'Whether this is the last chunk (default: False).' + ), + }) + .describe('Echo node that emits an A2A event to update the stored task.'); // ── Schema ────────────────────────────────────────────────────────── @@ -570,7 +610,6 @@ export const AgentFabricSchemaInfo: SchemaInfo = { }, namespacedFunctions: { a2a: new Set([ - 'task', 'message', 'textPart', 'parts', diff --git a/dialect/agentfabric/src/tests/dialect.test.ts b/dialect/agentfabric/src/tests/dialect.test.ts index b8229e4a..9e2af363 100644 --- a/dialect/agentfabric/src/tests/dialect.test.ts +++ b/dialect/agentfabric/src/tests/dialect.test.ts @@ -143,7 +143,8 @@ router route: it('parses echo block', () => { const source = ` echo response: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "Done!" `; const doc = parseDocument(source); @@ -186,7 +187,8 @@ orchestrator main: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "All done" `; const doc = parseDocument(source); diff --git a/dialect/agentfabric/src/tests/lint.test.ts b/dialect/agentfabric/src/tests/lint.test.ts index 6f99ca42..7d56b5c5 100644 --- a/dialect/agentfabric/src/tests/lint.test.ts +++ b/dialect/agentfabric/src/tests/lint.test.ts @@ -41,7 +41,8 @@ trigger t: on_message: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -181,7 +182,8 @@ trigger t: on_message: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -219,7 +221,8 @@ generator g: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -242,7 +245,8 @@ router r: - target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -271,7 +275,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -301,7 +306,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -331,7 +337,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -361,7 +368,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -388,7 +396,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -415,7 +424,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -442,7 +452,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -469,7 +480,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -496,7 +508,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -523,7 +536,8 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -563,7 +577,8 @@ subagent s: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -633,7 +648,8 @@ subagent s: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -708,7 +724,8 @@ router countryRouter: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -726,8 +743,9 @@ echo done: it('does not accept A2A global calls with @', () => { const source = ` echo successResponse: - kind: "a2a:response" - task: @a2a.task({ state: "completed", message: @a2a.message()}) + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: @a2a.message() `; const result = parseAndLintSource(source); expect( @@ -736,7 +754,7 @@ echo successResponse: d.code === 'namespace-function-call' && d.message.includes('Only direct namespace function calls are allowed') ).length - ).toBe(2); + ).toBe(1); }); it('allows namespaced A2A helper calls in expression fields (a2a.message, a2a.textPart, …)', () => { @@ -748,7 +766,8 @@ trigger t: transition to @echo.out echo out: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: a2a.message(a2a.textPart("hello")) `; const result = parseAndLintSource(source); @@ -759,11 +778,14 @@ echo out: const source = ` executor step: do: -> - set @variables.t = a2a.task({ state: "completed" }) + set @variables.t = a2a.message({ parts: [a2a.textPart("hello")] }) `; const result = parseAndLintSource(source); const relevant = result.diagnostics.filter( - d => d.code !== 'unused-node' && d.code !== 'missing-required-field' + d => + d.code !== 'unused-node' && + d.code !== 'missing-required-field' && + d.code !== 'terminal-requires-status-update' ); expect(relevant.length).toBe(0); }); @@ -790,7 +812,8 @@ generator g: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -843,7 +866,8 @@ orchestrator o: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -886,7 +910,8 @@ orchestrator o: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -935,7 +960,8 @@ orchestrator o: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1002,7 +1028,8 @@ orchestrator o: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1044,7 +1071,8 @@ orchestrator o: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1090,7 +1118,8 @@ subagent A: transition to @echo.B echo B: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const { diagnostics } = parseAndLintSource(source); @@ -1109,7 +1138,8 @@ trigger t: transition to @echo.X echo X: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const { diagnostics } = parseAndLintSource(source); @@ -1145,7 +1175,8 @@ subagent A: transition to @echo.X echo X: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const { diagnostics } = parseAndLintSource(source); @@ -1171,11 +1202,13 @@ router r: target: @echo.X echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" echo X: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "fallback" `; const { diagnostics } = parseAndLintSource(source); @@ -1201,11 +1234,13 @@ router r: target: @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" echo X: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "x" `; const { diagnostics } = parseAndLintSource(source); @@ -1271,11 +1306,13 @@ generator unusedGen: transition to @echo.done echo unusedEcho: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "x" echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const { diagnostics } = parseAndLintSource(source); @@ -1323,7 +1360,8 @@ subagent A: transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const { diagnostics } = parseAndLintSource(source); @@ -1403,7 +1441,8 @@ orchestrator D: transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1483,7 +1522,8 @@ trigger t: transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" orchestrator A: @@ -1568,7 +1608,8 @@ orchestrator C: transition to @orchestrator.A echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1615,7 +1656,8 @@ executor run_billing: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1647,7 +1689,8 @@ executor run_it: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1673,7 +1716,8 @@ executor run_it: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1720,7 +1764,8 @@ subagent worker: on_exit: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1745,7 +1790,8 @@ trigger t: on_message: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1774,7 +1820,8 @@ trigger t: on_message: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1810,7 +1857,8 @@ trigger t: on_message: -> transition to @echo.done echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1826,7 +1874,8 @@ config: agent_name: "unknown-field-test" echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" bogus_field: "should error" `; @@ -1849,7 +1898,8 @@ router r: when: true echo done: - kind: "a2a:response" + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" message: "ok" `; const result = parseAndLintSource(source); @@ -1861,4 +1911,135 @@ echo done: true ); }); + + it('reports terminal-requires-status-update when terminal node is not an echo', () => { + const source = ` +config: + agent_name: "terminal-no-echo" + +llm: + g: + target: "llm://conn" + kind: "OpenAI" + model: "gpt-4" + +trigger t: + kind: "a2a" + target: "brokers://terminal-no-echo/a2a" + on_message: -> transition to @subagent.s + +subagent s: + llm: @llm.g + reasoning: + instructions: -> do work +`; + const result = parseAndLintSource(source); + expect( + result.diagnostics.some(d => d.code === 'terminal-requires-status-update') + ).toBe(true); + }); + + it('reports terminal-requires-status-update when terminal echo has non-terminal state', () => { + const source = ` +config: + agent_name: "non-terminal-state" + +trigger t: + kind: "a2a" + target: "brokers://non-terminal-state/a2a" + on_message: -> transition to @echo.done + +echo done: + kind: "a2a:status_update_event" + state: "TASK_STATE_WORKING" + message: "still going" +`; + const result = parseAndLintSource(source); + expect( + result.diagnostics.some(d => d.code === 'terminal-requires-status-update') + ).toBe(true); + }); + + it('does not report terminal-requires-status-update for valid terminal echo', () => { + const source = ` +config: + agent_name: "valid-terminal" + +trigger t: + kind: "a2a" + target: "brokers://valid-terminal/a2a" + on_message: -> transition to @echo.done + +echo done: + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: "ok" +`; + const result = parseAndLintSource(source); + expect( + result.diagnostics.some(d => d.code === 'terminal-requires-status-update') + ).toBe(false); + }); + + it('does not report error when status echo is followed by another node', () => { + const source = ` +config: + agent_name: "echo-then-more" + +trigger t: + kind: "a2a" + target: "brokers://echo-then-more/a2a" + on_message: -> transition to @echo.status + +echo status: + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: "done" + on_exit: -> + transition to @echo.artifact + +echo artifact: + kind: "a2a:artifact_update_event" + artifact: a2a.artifact({name: "log", parts: [a2a.textPart("cleanup")]}) +`; + const result = parseAndLintSource(source); + const relevant = result.diagnostics.filter( + d => d.code === 'terminal-requires-status-update' + ); + expect(relevant.length).toBe(0); + }); + + it('reports echo-invalid-state for invalid state value', () => { + const source = ` +config: + agent_name: "bad-state" + +trigger t: + kind: "a2a" + target: "brokers://bad-state/a2a" + on_message: -> transition to @echo.done + +echo done: + kind: "a2a:status_update_event" + state: "running" + message: "ok" +`; + const result = parseAndLintSource(source); + expect(result.diagnostics.some(d => d.code === 'echo-invalid-state')).toBe( + true + ); + }); + + it('reports unknown-variant for removed a2a:response kind', () => { + const source = ` +echo done: + kind: "a2a:response" + state: "TASK_STATE_COMPLETED" + message: "ok" +`; + const result = parseAndLintSource(source); + expect(result.diagnostics.some(d => d.code === 'unknown-variant')).toBe( + true + ); + }); }); diff --git a/dialect/agentfabric/src/tests/resources/agentfabric-customer-support-netwrok.agent b/dialect/agentfabric/src/tests/resources/agentfabric-customer-support-netwrok.agent index d3163927..5642cd74 100644 --- a/dialect/agentfabric/src/tests/resources/agentfabric-customer-support-netwrok.agent +++ b/dialect/agentfabric/src/tests/resources/agentfabric-customer-support-netwrok.agent @@ -147,16 +147,26 @@ orchestrator general_response: transition to @echo.send_response echo send_response: - kind: "a2a:response" - task: a2a.task({ - state: "completed", - message: a2a.message({ - parts: [ - a2a.textPart(@echo.send_response.input) - ] - }) - } - ) + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: a2a.message({ + parts: [ + a2a.textPart(@echo.send_response.input) + ] + }) + on_exit: -> + transition to @echo.send_artifact + +echo send_artifact: + kind: "a2a:artifact_update_event" + artifact: a2a.artifact({ + name: "support_response", + parts: [ + a2a.textPart(@echo.send_response.input) + ] + }) + append: False + lastChunk: True on_exit: -> transition to @executor.cleanup diff --git a/dialect/agentfabric/src/tests/resources/agentfabric-customer-support-netwrok.yaml b/dialect/agentfabric/src/tests/resources/agentfabric-customer-support-netwrok.yaml index a9b0d128..d0a87dc7 100644 --- a/dialect/agentfabric/src/tests/resources/agentfabric-customer-support-netwrok.yaml +++ b/dialect/agentfabric/src/tests/resources/agentfabric-customer-support-netwrok.yaml @@ -368,7 +368,7 @@ unifiedAgentSpec: tools: - ref: IdentityAction state-updates: - - __send_response_value: a2a_task(state="completed", + - __send_response_value: a2a_status_update(state="TASK_STATE_COMPLETED", message=a2a_message(parts=[a2a_textPart(state._node_input)])) - outputs: add(state.outputs, "send_response", state.__send_response_value) on-init: @@ -376,6 +376,26 @@ unifiedAgentSpec: ref: IdentityAction state-updates: - _node_input: get(system.node_outputs, state._handoff_source, '') + on-exit: + - type: handoff + target: send_artifact + add-tool-result-to-chat-history: false + output-template: null + - name: send_artifact + type: action + label: null + tools: + - ref: IdentityAction + state-updates: + - __send_artifact_value: a2a_artifact_update(artifact=a2a_artifact(name="support_response", + parts=[a2a_textPart(state._node_input)]), append=false, + lastChunk=true) + - outputs: add(state.outputs, "send_artifact", state.__send_artifact_value) + on-init: + - type: action + ref: IdentityAction + state-updates: + - _node_input: get(system.node_outputs, state._handoff_source, '') on-exit: - type: handoff target: cleanup diff --git a/dialect/agentfabric/src/tests/resources/it-help-investigation.agent b/dialect/agentfabric/src/tests/resources/it-help-investigation.agent index 800900bd..faa43597 100644 --- a/dialect/agentfabric/src/tests/resources/it-help-investigation.agent +++ b/dialect/agentfabric/src/tests/resources/it-help-investigation.agent @@ -129,16 +129,13 @@ executor escalateTicket: transition to @echo.escalationResponse echo escalationResponse: - kind: "a2a:response" - task: a2a.task({ - state: "completed", - message: a2a.message({ - messageId: uuid(), - parts: [ - a2a.textPart("Ticket " + @generator.classifySeverity.output.ticket_id + " has been escalated to the on-call team due to high severity: " + @generator.classifySeverity.output.reason) - ] - }), - metadata: None + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: a2a.message({ + messageId: uuid(), + parts: [ + a2a.textPart("Ticket " + @generator.classifySeverity.output.ticket_id + " has been escalated to the on-call team due to high severity: " + @generator.classifySeverity.output.reason) + ] }) @@ -209,16 +206,13 @@ generator helpSummary: transition to @echo.helpResponse echo helpResponse: - kind: "a2a:response" - task: a2a.task({ - state: "completed", - message: a2a.message({ - messageId: uuid(), - parts: [ - a2a.textPart(generator.helpSummary.output) - ] - }), - metadata: None + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: a2a.message({ + messageId: uuid(), + parts: [ + a2a.textPart(generator.helpSummary.output) + ] }) @@ -234,16 +228,13 @@ generator licenseSummary: transition to @echo.licenseResponse echo licenseResponse: - kind: "a2a:response" - task: a2a.task({ - state: "completed", - message: a2a.message({ - messageId: uuid(), - parts: [ - a2a.textPart(@generator.licenseSummary.output) - ] - }), - metadata: None + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: a2a.message({ + messageId: uuid(), + parts: [ + a2a.textPart(@generator.licenseSummary.output) + ] }) @@ -261,14 +252,11 @@ executor escalateUnresolved: transition to @echo.unresolvedResponse echo unresolvedResponse: - kind: "a2a:response" - task: a2a.task({ - state: "completed", - message: a2a.message({ - messageId: uuid(), - parts: [ - a2a.textPart("Ticket " + @generator.classifySeverity.output.ticket_id + " could not be resolved automatically and has been escalated to a human agent. Summary: " + @orchestrator.crossPlatformTriage.output.summary) - ] - }), - metadata: None + kind: "a2a:status_update_event" + state: "TASK_STATE_COMPLETED" + message: a2a.message({ + messageId: uuid(), + parts: [ + a2a.textPart("Ticket " + @generator.classifySeverity.output.ticket_id + " could not be resolved automatically and has been escalated to a human agent. Summary: " + @orchestrator.crossPlatformTriage.output.summary) + ] }) \ No newline at end of file diff --git a/dialect/agentfabric/src/tests/resources/it-help-investigation.graph.json b/dialect/agentfabric/src/tests/resources/it-help-investigation.graph.json index 8a00c346..a9b5bb02 100644 --- a/dialect/agentfabric/src/tests/resources/it-help-investigation.graph.json +++ b/dialect/agentfabric/src/tests/resources/it-help-investigation.graph.json @@ -13,8 +13,8 @@ "kind": "orchestrator", "additionalProperties": { "label": "Cross-Platform Triage", - "lexical-start-position": "147,2", - "lexical-end-position": "181,42" + "lexical-start-position": "144,2", + "lexical-end-position": "178,42" } }, { @@ -30,16 +30,16 @@ "id": "generator.helpSummary", "kind": "generator", "additionalProperties": { - "lexical-start-position": "202,2", - "lexical-end-position": "208,36" + "lexical-start-position": "199,2", + "lexical-end-position": "205,36" } }, { "id": "generator.licenseSummary", "kind": "generator", "additionalProperties": { - "lexical-start-position": "227,2", - "lexical-end-position": "233,39" + "lexical-start-position": "221,2", + "lexical-end-position": "227,39" } }, { @@ -54,8 +54,8 @@ "id": "executor.escalateUnresolved", "kind": "executor", "additionalProperties": { - "lexical-start-position": "252,2", - "lexical-end-position": "260,42" + "lexical-start-position": "243,2", + "lexical-end-position": "251,42" } }, { @@ -72,8 +72,8 @@ "kind": "router", "additionalProperties": { "outputs": "License Given, Unresolved, otherwise", - "lexical-start-position": "187,2", - "lexical-end-position": "196,34" + "lexical-start-position": "184,2", + "lexical-end-position": "193,34" } }, { @@ -81,31 +81,31 @@ "kind": "echo", "additionalProperties": { "lexical-start-position": "131,2", - "lexical-end-position": "141,4" + "lexical-end-position": "138,4" } }, { "id": "echo.helpResponse", "kind": "echo", "additionalProperties": { - "lexical-start-position": "211,2", - "lexical-end-position": "221,4" + "lexical-start-position": "208,2", + "lexical-end-position": "215,4" } }, { "id": "echo.licenseResponse", "kind": "echo", "additionalProperties": { - "lexical-start-position": "236,2", - "lexical-end-position": "246,4" + "lexical-start-position": "230,2", + "lexical-end-position": "237,4" } }, { "id": "echo.unresolvedResponse", "kind": "echo", "additionalProperties": { - "lexical-start-position": "263,2", - "lexical-end-position": "273,4" + "lexical-start-position": "254,2", + "lexical-end-position": "261,4" } } ], @@ -122,8 +122,8 @@ "from": "orchestrator.crossPlatformTriage", "to": "router.resolutionRouter", "additionalProperties": { - "lexical-start-position": "181,15", - "lexical-end-position": "181,42" + "lexical-start-position": "178,15", + "lexical-end-position": "178,42" } }, { @@ -138,16 +138,16 @@ "from": "generator.helpSummary", "to": "echo.helpResponse", "additionalProperties": { - "lexical-start-position": "208,15", - "lexical-end-position": "208,36" + "lexical-start-position": "205,15", + "lexical-end-position": "205,36" } }, { "from": "generator.licenseSummary", "to": "echo.licenseResponse", "additionalProperties": { - "lexical-start-position": "233,15", - "lexical-end-position": "233,39" + "lexical-start-position": "227,15", + "lexical-end-position": "227,39" } }, { @@ -162,8 +162,8 @@ "from": "executor.escalateUnresolved", "to": "echo.unresolvedResponse", "additionalProperties": { - "lexical-start-position": "260,15", - "lexical-end-position": "260,42" + "lexical-start-position": "251,15", + "lexical-end-position": "251,42" } }, { @@ -191,8 +191,8 @@ "additionalProperties": { "output": "License Given", "predicate": "@orchestrator.crossPlatformTriage.output.resolution == \"license_given\"", - "lexical-start-position": "189,14", - "lexical-end-position": "189,39" + "lexical-start-position": "186,14", + "lexical-end-position": "186,39" } }, { @@ -201,8 +201,8 @@ "additionalProperties": { "output": "Unresolved", "predicate": "@orchestrator.crossPlatformTriage.output.resolution == \"unresolved\"", - "lexical-start-position": "192,14", - "lexical-end-position": "192,42" + "lexical-start-position": "189,14", + "lexical-end-position": "189,42" } }, { @@ -210,8 +210,8 @@ "to": "generator.helpSummary", "additionalProperties": { "output": "otherwise", - "lexical-start-position": "196,12", - "lexical-end-position": "196,34" + "lexical-start-position": "193,12", + "lexical-end-position": "193,34" } } ] diff --git a/dialect/agentfabric/src/tests/resources/it-help-investigation.yaml b/dialect/agentfabric/src/tests/resources/it-help-investigation.yaml index c13d1ce7..2087582c 100644 --- a/dialect/agentfabric/src/tests/resources/it-help-investigation.yaml +++ b/dialect/agentfabric/src/tests/resources/it-help-investigation.yaml @@ -438,14 +438,14 @@ unifiedAgentSpec: tools: - ref: IdentityAction state-updates: - - __escalationResponse_value: "a2a_task(state=\"completed\", + - __escalationResponse_value: "a2a_status_update(state=\"TASK_STATE_COMPLETED\", message=a2a_message(messageId=uuid(), parts=[a2a_textPart(\"Ticket \" + parse_json(system.node_outputs['classifySeverity']).ticket_id + \" has been escalated to the on-call team due to high severity: \" + - parse_json(system.node_outputs['classifySeverity']).reason)]), - metadata=None)" + parse_json(system.node_outputs['classifySeverity']).reason)])\ + )" - outputs: add(state.outputs, "escalationResponse", state.__escalationResponse_value) on-init: null @@ -458,10 +458,9 @@ unifiedAgentSpec: tools: - ref: IdentityAction state-updates: - - __helpResponse_value: a2a_task(state="completed", + - __helpResponse_value: a2a_status_update(state="TASK_STATE_COMPLETED", message=a2a_message(messageId=uuid(), - parts=[a2a_textPart(generator.helpSummary.output)]), - metadata=None) + parts=[a2a_textPart(generator.helpSummary.output)])) - outputs: add(state.outputs, "helpResponse", state.__helpResponse_value) on-init: null on-exit: null @@ -473,10 +472,9 @@ unifiedAgentSpec: tools: - ref: IdentityAction state-updates: - - __licenseResponse_value: a2a_task(state="completed", + - __licenseResponse_value: a2a_status_update(state="TASK_STATE_COMPLETED", message=a2a_message(messageId=uuid(), - parts=[a2a_textPart(system.node_outputs['licenseSummary'])]), - metadata=None) + parts=[a2a_textPart(system.node_outputs['licenseSummary'])])) - outputs: add(state.outputs, "licenseResponse", state.__licenseResponse_value) on-init: null on-exit: null @@ -488,14 +486,14 @@ unifiedAgentSpec: tools: - ref: IdentityAction state-updates: - - __unresolvedResponse_value: "a2a_task(state=\"completed\", + - __unresolvedResponse_value: "a2a_status_update(state=\"TASK_STATE_COMPLETED\", message=a2a_message(messageId=uuid(), parts=[a2a_textPart(\"Ticket \" + parse_json(system.node_outputs['classifySeverity']).ticket_id + \" could not be resolved automatically and has been escalated to a human agent. Summary: \" + parse_json(system.node_outputs['crossPlatformTriage']).summar\ - y)]), metadata=None)" + y)]))" - outputs: add(state.outputs, "unresolvedResponse", state.__unresolvedResponse_value) on-init: null diff --git a/dialect/agentfabric/src/tests/with-completions.test.ts b/dialect/agentfabric/src/tests/with-completions.test.ts index 30d82678..d9a5176a 100644 --- a/dialect/agentfabric/src/tests/with-completions.test.ts +++ b/dialect/agentfabric/src/tests/with-completions.test.ts @@ -70,7 +70,7 @@ describe('getWithCompletions with agentfabric', () => { ' transition to @echo.done', 'echo:', ' done:', - ' kind: "a2a:response"', + ' kind: "a2a:status_update_event"', ].join('\n'); const result = parseAndLintSource(source); From 065e73e4b07f0197a660c194767b0b94bef7e7f0 Mon Sep 17 00:00:00 2001 From: "ashwin.venkataraman" Date: Wed, 10 Jun 2026 22:35:23 +0530 Subject: [PATCH 2/2] add changelog --- dialect/agentfabric/CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/dialect/agentfabric/CHANGELOG.md b/dialect/agentfabric/CHANGELOG.md index c358a8bd..878acf1a 100644 --- a/dialect/agentfabric/CHANGELOG.md +++ b/dialect/agentfabric/CHANGELOG.md @@ -1,5 +1,23 @@ # @agentscript/agentfabric-dialect +## 0.4.0 + +### Breaking Changes + +- Redesigned the `echo` node around A2A v1 task-update events. The single `kind: "a2a:response"` variant is replaced by two event variants: + + - `a2a:status_update_event` — sets the task `state` (required; one of the A2A v1 `TASK_STATE_*` values) with an optional `message`. + - `a2a:artifact_update_event` — emits an `artifact` (required) with optional `append` and `lastChunk` flags. + + The base `task` and `artifacts` fields and the `a2a.task` namespaced function have been removed. Flows using `kind: "a2a:response"`, `task:`, `artifacts:`, or `a2a.task(...)` must migrate to the new event variants. + +### Minor Changes + +- Added the `terminal-requires-status-update` lint rule: every terminal branch in a graph must reach an `a2a:status_update_event` echo that sets a terminal state (`TASK_STATE_COMPLETED`, `TASK_STATE_FAILED`, or `TASK_STATE_CANCELED`). The echo need not be the leaf node. +- Added the `echo-invalid-state` lint rule, which validates that an `a2a:status_update_event` echo's `state` is a known A2A v1 task state. +- Exported `A2A_TASK_STATES` and `A2A_TERMINAL_STATES` from the schema for shared use across the schema, compiler, and linter. +- Removed the `echo-task-or-message-required` rule, which no longer applies under the new event model. + ## 0.1.24 ### Patch Changes