@ziggs-ai/agent-sdk 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-07-30
4
+
5
+ ### Changed (breaking)
6
+
7
+ - `task-update` / `task_set_result` accept `state` only — legacy `status` alias removed (ZIG-1057).
8
+ - Removed export `syncAggregateTaskViews`; read pending/collected/delegated via `allPendingSubtasks` / `allCollectedResults` / `allDelegatedAgreementIds`. Ctx aggregate mirrors stay empty.
9
+
3
10
  ## 0.4.0 — 2026-07-23
4
11
 
5
12
  ### Added
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export { KICKOFF_STATE, injectKickoffDefault, buildKickoffState } from './runtim
34
34
  export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
35
35
  export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
36
36
  export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
37
- export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, syncAggregateTaskViews, } from './runtime/taskBookkeeping.js';
37
+ export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
38
38
  export { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
39
39
  export type { Outcome, OutcomeKind, Ctx, Identity, HistoryEntry, ToolResultEntry, PendingSubtask, CollectedSubtaskResult, TaskBookkeeping, Workflow, State, ParkedState, ThinkingState, MechanicalState, Action, Transition, PromptDef, ActionPrompt, Produces, ProducesFn, AgreementRef, TaskRef, ProposalRef, AgentConfig, DefineAgentInput, ServicesConfig, Effect, EffectKind, EffectResult, EffectResultMap, EffectHandler, LlmMessage, LlmToolCall, LlmToolSchema, LlmResponse, ContextSnapshot, ReadContextOpts, ToolCallResult, RecordedEntry, WakeWriteKeys, } from './types.js';
40
40
  export { default } from './AgentHost.js';
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ export { KICKOFF_STATE, injectKickoffDefault, buildKickoffState } from './runtim
28
28
  export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
29
29
  export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
30
30
  export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
31
- export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, syncAggregateTaskViews, } from './runtime/taskBookkeeping.js';
31
+ export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
32
32
  export { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
33
33
  export { default } from './AgentHost.js';
34
34
  // ── Supporting modules ──────────────────────────────────────────────────────
@@ -1,4 +1,4 @@
1
- import { migrateLegacyTaskFields, syncAggregateTaskViews } from './taskBookkeeping.js';
1
+ import { migrateLegacyTaskFields } from './taskBookkeeping.js';
2
2
  /**
3
3
  * The one place a per-lane `Ctx` is constructed.
4
4
  *
@@ -18,16 +18,15 @@ import { migrateLegacyTaskFields, syncAggregateTaskViews } from './taskBookkeepi
18
18
  export function bootstrapCtx(identity, existing) {
19
19
  if (existing) {
20
20
  const legacy = existing;
21
- const ctx = {
21
+ return {
22
22
  ...existing,
23
23
  identity,
24
24
  tasks: migrateLegacyTaskFields(legacy),
25
+ // ZIG-1057: deprecated mirrors stay empty; use all*() readers.
25
26
  pendingSubtasks: {},
26
27
  collectedResults: [],
27
28
  delegatedAgreementIds: [],
28
29
  };
29
- syncAggregateTaskViews(ctx);
30
- return ctx;
31
30
  }
32
31
  return {
33
32
  identity,
@@ -1,6 +1,6 @@
1
1
  import { isProtocolToolName, mapProtocolToolToOperation } from '../tasks/protocolRegistry.js';
2
2
  import { getBatchEvents, isTaskResultRelevantToAgent } from '../context/batch.js';
3
- import { bookForDelegation, ensureTaskBook, findBookHoldingPending, removeTaskBook, syncAggregateTaskViews, } from './taskBookkeeping.js';
3
+ import { bookForDelegation, ensureTaskBook, findBookHoldingPending, removeTaskBook, } from './taskBookkeeping.js';
4
4
  // ── outcomeFromEvent: incoming wire event → Outcome ──────────────────────────
5
5
  /**
6
6
  * Converts a raw incoming event into the single Outcome that re-enters the
@@ -480,5 +480,4 @@ export function applyOutcomeToCtx(ctx, outcome) {
480
480
  default:
481
481
  break;
482
482
  }
483
- syncAggregateTaskViews(ctx);
484
483
  }
@@ -26,10 +26,11 @@ export declare function hasDelegatedAgreement(ctx: Ctx, agreementId: string): bo
26
26
  * focused task. Leaves sibling entries (and the coordinator lane bucket) alone.
27
27
  */
28
28
  export declare function removeTaskBook(ctx: Ctx, taskId: string): void;
29
- /** Refresh deprecated aggregate views from the per-task map. */
30
- export declare function syncAggregateTaskViews(ctx: Ctx): void;
31
29
  /**
32
30
  * Migrate a pre-ZIG-977 persisted ctx (lane-level pendingSubtasks /
33
31
  * collectedResults / delegatedAgreementIds) into the tasks map.
32
+ * ZIG-1057: the deprecated lane aggregates are no longer auto-synced —
33
+ * readers use allPendingSubtasks / allCollectedResults / allDelegatedAgreementIds.
34
+ * This migrator stays so a session persisted before the tasks map still boots.
34
35
  */
35
36
  export declare function migrateLegacyTaskFields(existing: Record<string, unknown>): Record<string, TaskBookkeeping>;
@@ -106,15 +106,12 @@ export function removeTaskBook(ctx, taskId) {
106
106
  ctx.activeAgreementId = nextId ? (ctx.tasks[nextId]?.agreementId ?? null) : null;
107
107
  }
108
108
  }
109
- /** Refresh deprecated aggregate views from the per-task map. */
110
- export function syncAggregateTaskViews(ctx) {
111
- ctx.pendingSubtasks = allPendingSubtasks(ctx);
112
- ctx.collectedResults = allCollectedResults(ctx);
113
- ctx.delegatedAgreementIds = allDelegatedAgreementIds(ctx);
114
- }
115
109
  /**
116
110
  * Migrate a pre-ZIG-977 persisted ctx (lane-level pendingSubtasks /
117
111
  * collectedResults / delegatedAgreementIds) into the tasks map.
112
+ * ZIG-1057: the deprecated lane aggregates are no longer auto-synced —
113
+ * readers use allPendingSubtasks / allCollectedResults / allDelegatedAgreementIds.
114
+ * This migrator stays so a session persisted before the tasks map still boots.
118
115
  */
119
116
  export function migrateLegacyTaskFields(existing) {
120
117
  if (existing.tasks && typeof existing.tasks === 'object') {
@@ -183,11 +183,9 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
183
183
  });
184
184
  }
185
185
  case 'task-update': {
186
- // ZIG-957: canonical tool param is `state` (MCP + REST). Accept legacy
187
- // `status` so fleet prompts that still emit it keep working.
188
- const terminalRaw = (typeof payload.state === 'string' && payload.state) ||
189
- (typeof payload.status === 'string' && payload.status) ||
190
- '';
186
+ // ZIG-1057: `state` only — legacy `status` alias removed. Fleet prompts
187
+ // must emit state (coffee / fruit / ledger / agent-definer).
188
+ const terminalRaw = typeof payload.state === 'string' && payload.state ? payload.state : '';
191
189
  if (!payload.taskId || !terminalRaw)
192
190
  return null;
193
191
  const state = normalizeState(terminalRaw);
@@ -34,8 +34,8 @@ function handlerFor(operation) {
34
34
  }
35
35
  export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service', 'link'], description: "Contract type. Defaults to service. 'link' proposes a bilateral trust link (no work, no money)." }, parentAgreementId: 'string', parentTaskId: 'string', providerId: { type: 'string', description: 'Who does the work — required on direct proposals. Pass your own id when YOU are offering to do the work, or the proposedTo id when you are commissioning the RECIPIENT (any other third party needs a matching published offer). On a broadcast: omit for a quest (the claimer works, you pay), or pass your own id to publish a standing offer (you work, the claimer pays). The payer is always derived as the non-providing side.' }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00. Quote it to a human in the same units the agreement charges.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', billing: { type: 'string', enum: ['total', 'per_task'], description: "How price reads: 'total' (default, escrowed and paid at the end) or 'per_task' (a rate settled as each task completes — standing agreements only, and the default for a hire)." }, agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean' }, handlerFor('agreement-propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay") on direct proposals. Broadcast with proposedTo="everyone" (fully public) or "org" (your org only): no providerId publishes a QUEST any agent may claim; providerId = your own id publishes a STANDING OFFER (claiming is agreement_claim; browsing is marketplace_view). engagementKind "link" with proposedTo = an agent id proposes bilateral trust. engagementKind defaults to service. Agreements are STANDING by default (no expiry, unlimited tasks under one agreement); set expiresAt or maxExecutions only when the engagement should end on its own.', isAgreementCreation: true });
36
36
  export const agreementSubcontractTool = defineTool('agreement_subcontract', { description: { type: 'string', required: true }, executorId: { type: 'string', required: true }, parentAgreementId: { type: 'string', required: true }, parentTaskId: 'string', payerId: 'string', price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00. Quote it to a human in the same units the agreement charges.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean' }, handlerFor('agreement-subcontract'), { description: 'Delegate work to another agent under an existing parent agreement. Requires parentAgreementId.', isAgreementCreation: true });
37
- export const agreementRespondTool = defineTool('agreement_respond', { agreementId: { type: 'string', required: true }, action: { type: 'string', required: true, enum: ['approve', 'reject'] } }, handlerFor('agreement-respond'), { description: 'Approve or reject a pending DIRECT agreement proposal addressed to you. Open broadcasts (quests, standing offers, link invites) have no personal approval slot — claim those with agreement_claim instead, or ignore them to pass.' });
38
- export const agreementClaimTool = defineTool('agreement_claim', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (quest / offer / link invite id)' } }, handlerFor('agreement-claim'), { description: 'Claim an open broadcast agreement by id — a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. Find quests/offers with marketplace_view; direct proposals are approved with agreement_respond instead, not claimed.' });
37
+ export const agreementRespondTool = defineTool('agreement_respond', { agreementId: { type: 'string', required: true }, action: { type: 'string', required: true, enum: ['approve', 'reject'] } }, handlerFor('agreement-respond'), { description: 'Approve or reject a pending DIRECT agreement proposal addressed to you. Open broadcasts (quests, standing offers, link invites) have no personal approval slot — claim those with agreement_claim instead, or ignore them to pass. ONE exception: a hand-off that pins YOU as provider carries your pending approval even as an open broadcast — approving consents to serving whoever claims it (the row stays open for claims); rejecting cancels the hand-off. Ignoring it leaves the hand-off unclaimable, so always respond.' });
38
+ export const agreementClaimTool = defineTool('agreement_claim', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (quest / offer / link invite id)' } }, handlerFor('agreement-claim'), { description: 'Claim an open broadcast agreement by id — a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; direct proposals are approved with agreement_respond instead, not claimed.' });
39
39
  export const taskSetResultTool = defineTool('task_set_result', {
40
40
  taskId: { type: 'string', required: true },
41
41
  // ZIG-957: same param name as MCP ziggs_task_set_result / PATCH /tasks/:id/state.
@@ -50,7 +50,21 @@ export const agreementCounterTool = defineTool('agreement_counter', { agreementI
50
50
  export const agreementGetTool = defineTool('agreement_get', { agreementId: { type: 'string', required: true } }, handlerFor('agreement-check-proposal'), { description: 'Read the current proposal status for an agreement (pending, approved, rejected, countered, expired).' });
51
51
  export const agreementRevokeTool = defineTool('agreement_revoke', { agreementId: { type: 'string', required: true } }, handlerFor('agreement-revoke'), { description: 'Revoke a pending proposal you created so it can no longer be approved. Use when the user has clearly moved on to a different request before responding to the previous proposal card.' });
52
52
  export const agreementFulfillTool = defineTool('agreement_fulfill', { agreementId: { type: 'string', required: true } }, handlerFor('agreement-fulfill'), { description: 'END an agreement you provide — permanently. Fulfilling terminates the whole relationship, not one deliverable: every grant it conferred is revoked, its shared space is torn down, and it cannot be reopened. Finished WORK is reported via task_set_result (the task closes; the agreement stays standing for the next task). Only fulfill when the engagement\'s full scope is delivered and nothing more is expected — never a standing hire that just finished a task.' });
53
- export const taskUpdatePlanStepTool = defineTool('task_update_plan_step', { taskId: { type: 'string', required: true }, stepId: { type: 'string', required: true }, status: { type: 'string', required: true }, result: 'string' }, handlerFor('task-update-plan-step'), { description: "Update the status of a single step within a task's plan." });
53
+ // `status` names its four values here because nothing else does: the backend is
54
+ // the only place the vocabulary was written down, and it now rejects anything
55
+ // outside it. Every reader (the chat step badge, the SDK's own plan formatter)
56
+ // already treats an unrecognised status as unknown, so a guessed one was never
57
+ // going to render — this makes the model's choice a closed set instead.
58
+ export const taskUpdatePlanStepTool = defineTool('task_update_plan_step', {
59
+ taskId: { type: 'string', required: true },
60
+ stepId: { type: 'string', required: true },
61
+ status: {
62
+ type: 'string',
63
+ required: true,
64
+ enum: ['pending', 'in_progress', 'completed', 'skipped'],
65
+ },
66
+ result: 'string',
67
+ }, handlerFor('task-update-plan-step'), { description: "Update the status of a single step within a task's plan." });
54
68
  export const taskCreateTool = defineTool('task_create', { agreementId: { type: 'string', required: true }, description: { type: 'string', required: true }, parentTaskId: 'string', assigneeId: { type: 'string', description: 'Explicitly hand this task to one specific agent (e.g. a worker your orchestrator selected via agent search) instead of leaving it unassigned. Must be a party on the agreement (payer, provider, creator, or creator agent).' }, inputArtifactIds: { type: 'array', items: { type: 'string' }, description: 'ZIG-734: prior-step output artifact handles for structured input links.' }, plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean' }, handlerFor('task-spawn'), { description: 'Create a new task under an existing approved agreement. Use when the contract is already in place. Pass assigneeId to delegate the task to a specific agent rather than leaving it open.' });
55
69
  export const PROTOCOL_TOOLS = [
56
70
  agreementProposeTool, agreementSubcontractTool,
@@ -13,8 +13,10 @@ export interface RecordReportInput {
13
13
  taskId: string;
14
14
  report: TaskReport;
15
15
  /**
16
- * Primary artifact scope — required when `deliverable` is provided.
17
- * Pass exactly one of chatId or agreementId.
16
+ * Optional container for the deliverable artifact (ZIG-1037 — it used to be
17
+ * required). The task binding is what makes the deliverable retrievable and
18
+ * readable by the task's parties; a chat or agreement only widens where it also
19
+ * shows up. Pass at most one; when both arrive, agreementId wins.
18
20
  */
19
21
  chatId?: string;
20
22
  agreementId?: string;
@@ -10,21 +10,37 @@ import { ArtifactsClient, updateTaskState, } from '@ziggs-ai/api-client';
10
10
  */
11
11
  export async function recordReport(input) {
12
12
  const { creds, taskId, report, chatId, agreementId, deliverable } = input;
13
- if (deliverable && !chatId && !agreementId) {
14
- throw new Error('recordReport: chatId or agreementId is required when deliverable is provided');
13
+ // ZIG-1037: no container needed. The deliverable is bound to its task, which
14
+ // is what makes it retrievable as `via=task:<taskId>` and readable by the
15
+ // task's parties — a chat or agreement was never what made that work. Both
16
+ // containers at once IS still a mistake (only one takes effect), so resolve it
17
+ // here the way artifact_record does rather than letting the client throw
18
+ // at the last step of a finished task.
19
+ const scope = agreementId ? { agreementId } : chatId ? { chatId } : {};
20
+ // Deliverable FIRST, then close the task.
21
+ //
22
+ // The old order completed the task and then wrote the artifact with the
23
+ // soft-failing `write`, so a wire error left a task marked completed whose
24
+ // deliverable had silently vanished. Recording first means a failure here
25
+ // leaves the task open — the honest state, and one a retry can finish. The
26
+ // idempotency key is derived from the task, so that retry reuses the artifact
27
+ // row instead of recording the deliverable twice.
28
+ let artifactId;
29
+ if (deliverable) {
30
+ const artifactsClient = new ArtifactsClient(creds.operatorKey, creds.agentId);
31
+ ({ artifactId } = await artifactsClient.writeStrict({
32
+ text: deliverable,
33
+ content_type: 'result',
34
+ taskId,
35
+ ...scope,
36
+ visibility: 'chat',
37
+ // Deterministic, not random: one deliverable per task, so a replay of this
38
+ // whole function reproduces the key and dedups server-side.
39
+ idempotencyKey: `task-result:${taskId}`,
40
+ }));
15
41
  }
16
42
  await updateTaskState(taskId, 'completed', { result: { summary: report.summary, status: report.status ?? 'ok', links: report.links ?? [] } }, creds);
17
- if (!deliverable) {
18
- return { taskId };
19
- }
20
- const artifactsClient = new ArtifactsClient(creds.operatorKey, creds.agentId);
21
- await artifactsClient.write({
22
- text: deliverable,
23
- content_type: 'result',
24
- taskId,
25
- chatId,
26
- agreementId,
27
- visibility: 'chat',
28
- });
29
- return { taskId };
43
+ // The artifactId was being discarded even though RecordReportResult declares
44
+ // it — the caller had no way to reference the deliverable it just recorded.
45
+ return artifactId ? { taskId, artifactId } : { taskId };
30
46
  }
package/dist/types.d.ts CHANGED
@@ -188,13 +188,13 @@ export interface Ctx {
188
188
  activeAgreementId: string | null;
189
189
  pendingProposalAgreementId: string | null;
190
190
  /**
191
- * Aggregate views over `tasks` (all engagements in the lane). Kept in sync by
192
- * applyOutcomeToCtx / bootstrapCtx so existing workflow `when` guards that
193
- * read these fields (e.g. motifs awaitingAll) keep working after ZIG-977.
194
- * Prefer reading `tasks[taskId]` when you need per-task isolation.
191
+ * @deprecated ZIG-1057 — unused mirrors; always `{}` / `[]` after bootstrap.
192
+ * Read via `allPendingSubtasks` / `allCollectedResults` / `allDelegatedAgreementIds`.
195
193
  */
196
194
  pendingSubtasks: Record<string, PendingSubtask>;
195
+ /** @deprecated ZIG-1057 — see `pendingSubtasks`. */
197
196
  collectedResults: CollectedSubtaskResult[];
197
+ /** @deprecated ZIG-1057 — see `pendingSubtasks`. */
198
198
  delegatedAgreementIds: string[];
199
199
  toolResults: ToolResultEntry[];
200
200
  lastOutcome: Outcome;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/agent-sdk",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "@anthropic-ai/claude-agent-sdk": "^0.3.204",
27
27
  "@anthropic-ai/sdk": "^0.110.0",
28
28
  "@modelcontextprotocol/sdk": "^1.29.0",
29
- "@ziggs-ai/api-client": "^0.7.1",
29
+ "@ziggs-ai/api-client": "^0.8.0",
30
30
  "ajv": "^8.17.1",
31
31
  "dotenv": "^17.2.3",
32
32
  "openai": "^4.0.0",