@ziggs-ai/agent-sdk 0.1.3 → 0.1.4
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/README.md +1 -1
- package/package.json +9 -4
- package/src/AgentHost.ts +342 -0
- package/src/adapters/OpenAIAdapter.ts +125 -0
- package/src/agent/Agent.ts +98 -0
- package/src/cognition/validateContext.ts +95 -0
- package/src/context/applyEffects.ts +80 -0
- package/src/context/batch.ts +17 -0
- package/src/context/classifyEnvelope.ts +38 -0
- package/src/context/routingLabels.ts +46 -0
- package/src/defineAgent.ts +62 -0
- package/src/formatters/AgreementFormatter.ts +111 -0
- package/src/formatters/HistoryFormatter.ts +166 -0
- package/src/formatters/index.ts +2 -0
- package/src/index.ts +86 -0
- package/src/ingress/normalizeIncoming.ts +119 -0
- package/src/memory/MemoryStore.ts +104 -0
- package/src/runtime/AgentMachine.ts +298 -0
- package/src/runtime/PromptBuilder.ts +461 -0
- package/src/runtime/buildOutcome.ts +488 -0
- package/src/runtime/defaults.ts +72 -0
- package/src/runtime/runTurn.ts +637 -0
- package/src/runtime/validateWorkflow.ts +165 -0
- package/src/server/ConnectionPool.ts +155 -0
- package/src/server/EventQueue.ts +119 -0
- package/src/server/OutboxBuffer.ts +90 -0
- package/src/server/ZiggsEffectHandler.ts +335 -0
- package/src/server/agreements/AgreementService.ts +111 -0
- package/src/server/createHealthServer.ts +8 -0
- package/src/server/proactive/ProactiveTrigger.ts +83 -0
- package/src/server/runLauncher.ts +131 -0
- package/src/server/tasks/TaskService.ts +111 -0
- package/src/server/tasks/index.ts +4 -0
- package/src/server/tasks/paymentTools.ts +156 -0
- package/src/server/tasks/protocolRunner.ts +101 -0
- package/src/server/tasks/protocolTools.ts +96 -0
- package/src/server/ziggspay/ZiggsPayClient.ts +193 -0
- package/src/shared/ids.ts +3 -0
- package/src/shared/runtimeLog.ts +72 -0
- package/src/shared/types.ts +31 -0
- package/src/tasks/protocolRegistry.ts +25 -0
- package/src/tasks/taskCore.ts +139 -0
- package/src/tools/ToolManager.ts +95 -0
- package/src/tools/{ToolProvider.js → ToolProvider.ts} +5 -15
- package/src/tools/defineTool.ts +90 -0
- package/src/tools/index.ts +5 -0
- package/src/types.ts +368 -0
- package/src/utils/jsonExtractor.ts +100 -0
- package/src/ConnectionPool.js +0 -133
- package/src/adapters/OpenAIAdapter.js +0 -73
- package/src/agent/Agent.js +0 -121
- package/src/agent/EventQueue.js +0 -68
- package/src/agent/OutboxBuffer.js +0 -62
- package/src/cognition/PromptBuilder.js +0 -312
- package/src/cognition/resolveActionTool.js +0 -12
- package/src/cognition/runTurn.js +0 -578
- package/src/context/applyEffects.js +0 -133
- package/src/context/batch.js +0 -25
- package/src/context/classifyEnvelope.js +0 -82
- package/src/context/routingLabels.js +0 -54
- package/src/createHealthServer.js +0 -28
- package/src/formatters/HistoryFormatter.js +0 -257
- package/src/formatters/TaskFormatter.js +0 -180
- package/src/formatters/index.js +0 -9
- package/src/index.js +0 -76
- package/src/ingress/normalizeIncoming.js +0 -70
- package/src/runLauncher.js +0 -159
- package/src/shared/ids.js +0 -7
- package/src/shared/types.js +0 -86
- package/src/tasks/TaskService.js +0 -247
- package/src/tasks/index.js +0 -9
- package/src/tasks/taskCore.js +0 -229
- package/src/tasks/taskProtocolRegistry.js +0 -22
- package/src/tasks/taskProtocolRunner.js +0 -107
- package/src/tasks/taskProtocolTools.js +0 -87
- package/src/tools/ToolManager.js +0 -79
- package/src/tools/defineTool.js +0 -82
- package/src/tools/index.js +0 -11
- package/src/utils/jsonExtractor.js +0 -139
- package/src/workflow/AgentMachine.js +0 -250
- package/src/workflow/WorkflowRuntime.js +0 -63
- package/src/workflow/dsl.js +0 -287
- package/src/workflow/motifs.js +0 -435
- package/src/ziggs/runtime.js +0 -192
- /package/src/adapters/{index.js → index.ts} +0 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ziggs-ai/agent-sdk public surface.
|
|
3
|
+
*
|
|
4
|
+
* Foundation:
|
|
5
|
+
* • Outcome union — discriminated, hybrid (closed core + extension)
|
|
6
|
+
* • Workflow shape — explicit kind: 'parked' | 'thinking'
|
|
7
|
+
* • Visible defaults — thinkingDefaults({ initial }) spread by author
|
|
8
|
+
* • Validation at load — defineAgent throws on hard problems, warns on soft
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { defineAgent } from './defineAgent.js';
|
|
12
|
+
export { AgentHost, createAgent, createAgentPool } from './AgentHost.js';
|
|
13
|
+
export { Agent } from './agent/Agent.js';
|
|
14
|
+
export type { TickInput, TickOutput, AgentOptions } from './agent/Agent.js';
|
|
15
|
+
export type { SessionResolver, AgentHostOptions } from './AgentHost.js';
|
|
16
|
+
export { createZiggsEffectHandler } from './server/ZiggsEffectHandler.js';
|
|
17
|
+
export type { ZiggsEffectDeps } from './server/ZiggsEffectHandler.js';
|
|
18
|
+
export { EventQueue } from './server/EventQueue.js';
|
|
19
|
+
export { OutboxBuffer } from './server/OutboxBuffer.js';
|
|
20
|
+
export { normalizeIncomingEvent } from './ingress/normalizeIncoming.js';
|
|
21
|
+
|
|
22
|
+
// Runtime primitives
|
|
23
|
+
export { AgentMachine } from './runtime/AgentMachine.js';
|
|
24
|
+
export { runTurn } from './runtime/runTurn.js';
|
|
25
|
+
export { PromptBuilder } from './runtime/PromptBuilder.js';
|
|
26
|
+
export { thinkingDefaults, defineThinkingState, DEFAULT_WAIT_PROMPT } from './runtime/defaults.js';
|
|
27
|
+
export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
|
|
28
|
+
export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
|
|
29
|
+
|
|
30
|
+
// Types
|
|
31
|
+
export type {
|
|
32
|
+
Outcome, OutcomeKind,
|
|
33
|
+
Ctx, Identity, HistoryEntry, ToolResultEntry,
|
|
34
|
+
Workflow, State, ParkedState, ThinkingState,
|
|
35
|
+
Action, Transition, PromptDef, ActionPrompt, Produces, ProducesFn,
|
|
36
|
+
AgreementRef, TaskRef, ProposalRef,
|
|
37
|
+
AgentConfig, DefineAgentInput, ServicesConfig,
|
|
38
|
+
// Effects — Agent↔Server boundary
|
|
39
|
+
Effect, EffectKind, EffectResult, EffectResultMap, EffectHandler,
|
|
40
|
+
LlmMessage, LlmToolCall, LlmToolSchema, LlmResponse,
|
|
41
|
+
ContextSnapshot, ReadContextOpts, ToolCallResult, RecordedEntry,
|
|
42
|
+
} from './types.js';
|
|
43
|
+
|
|
44
|
+
export { default } from './AgentHost.js';
|
|
45
|
+
|
|
46
|
+
// ── Supporting modules ──────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export { validateContext, ContextWarning } from './cognition/validateContext.js';
|
|
49
|
+
export { runLauncher } from './server/runLauncher.js';
|
|
50
|
+
export { ConnectionPool } from './server/ConnectionPool.js';
|
|
51
|
+
export { createHealthServer } from './server/createHealthServer.js';
|
|
52
|
+
export { TaskService } from './server/tasks/TaskService.js';
|
|
53
|
+
export { AgreementService } from './server/agreements/AgreementService.js';
|
|
54
|
+
export { ToolManager } from './tools/ToolManager.js';
|
|
55
|
+
export { ToolProvider } from './tools/ToolProvider.js';
|
|
56
|
+
export { defineTool } from './tools/defineTool.js';
|
|
57
|
+
export { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
58
|
+
export { extractJSON, safeParseJSON } from './utils/jsonExtractor.js';
|
|
59
|
+
export { HistoryFormatter, historyFormatter, AgreementFormatter, agreementFormatter } from './formatters/index.js';
|
|
60
|
+
export { ProactiveTrigger } from './server/proactive/ProactiveTrigger.js';
|
|
61
|
+
export { ZiggsPayClient, createZiggsPayClient } from './server/ziggspay/ZiggsPayClient.js';
|
|
62
|
+
export {
|
|
63
|
+
PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool,
|
|
64
|
+
agreementRespondTool, agreementCounterProposalTool, agreementCheckProposalTool,
|
|
65
|
+
taskSpawnTool, taskUpdateTool, taskUpdatePlanStepTool,
|
|
66
|
+
} from './server/tasks/protocolTools.js';
|
|
67
|
+
export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool } from './server/tasks/paymentTools.js';
|
|
68
|
+
export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from './tasks/protocolRegistry.js';
|
|
69
|
+
export { dispatchProtocolOp } from './server/tasks/protocolRunner.js';
|
|
70
|
+
export { InMemoryStore, FileMemoryStore } from './memory/MemoryStore.js';
|
|
71
|
+
export type { MemoryStore } from './memory/MemoryStore.js';
|
|
72
|
+
export { classifyIncomingEvent } from './context/classifyEnvelope.js';
|
|
73
|
+
export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
|
|
74
|
+
export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
|
|
75
|
+
export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
|
|
76
|
+
export {
|
|
77
|
+
WebSocketClient,
|
|
78
|
+
MessagesClient,
|
|
79
|
+
ArtifactsClient,
|
|
80
|
+
ScopeClient,
|
|
81
|
+
AgentSearchClient,
|
|
82
|
+
getBackendUrl,
|
|
83
|
+
getWebSocketUrl,
|
|
84
|
+
} from '@ziggs-ai/api-client';
|
|
85
|
+
|
|
86
|
+
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { OPEN_AGREEMENT_TARGET } from '../tasks/taskCore.js';
|
|
2
|
+
|
|
3
|
+
export interface NormalizedEvent {
|
|
4
|
+
chatId: string | null;
|
|
5
|
+
event: {
|
|
6
|
+
senderId?: string;
|
|
7
|
+
senderType?: string;
|
|
8
|
+
receiverId?: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
type: 'task_result' | 'message';
|
|
11
|
+
result?: unknown;
|
|
12
|
+
text?: string;
|
|
13
|
+
};
|
|
14
|
+
shouldProcess: boolean;
|
|
15
|
+
reason: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface WireParties {
|
|
19
|
+
creatorId?: string;
|
|
20
|
+
providerId?: string;
|
|
21
|
+
payerId?: string;
|
|
22
|
+
proposedToId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WireTask {
|
|
26
|
+
taskId?: string;
|
|
27
|
+
agentId?: string;
|
|
28
|
+
executorId?: string;
|
|
29
|
+
payerId?: string;
|
|
30
|
+
proposedTo?: string;
|
|
31
|
+
createdBy?: string;
|
|
32
|
+
parentTaskId?: string;
|
|
33
|
+
state?: string;
|
|
34
|
+
proposal?: unknown;
|
|
35
|
+
agreement?: { parties?: WireParties };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface WireMetadata {
|
|
39
|
+
chatId?: string;
|
|
40
|
+
chat_id?: string;
|
|
41
|
+
entryType?: string;
|
|
42
|
+
content_type?: string;
|
|
43
|
+
contentType?: string;
|
|
44
|
+
receiverId?: string;
|
|
45
|
+
sender?: { id?: string; type?: string };
|
|
46
|
+
receiver?: { id?: string };
|
|
47
|
+
to?: { id?: string };
|
|
48
|
+
task?: WireTask;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface NormalizeArgs {
|
|
53
|
+
text: string;
|
|
54
|
+
metadata?: WireMetadata;
|
|
55
|
+
ownAgentId?: string | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeChatId(metadata: WireMetadata): string | null {
|
|
59
|
+
return metadata.chatId || metadata.chat_id || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeReceiverId(metadata: WireMetadata, taskData: WireTask | null): string | null {
|
|
63
|
+
return metadata.receiver?.id || metadata.receiverId || metadata.to?.id || taskData?.executorId || taskData?.proposedTo || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isTaskRelated(metadata: WireMetadata, taskData: WireTask | null): boolean {
|
|
67
|
+
if (!taskData?.taskId) return false;
|
|
68
|
+
const entryType = metadata.entryType || 'message';
|
|
69
|
+
const contentType = metadata.content_type || metadata.contentType || 'text';
|
|
70
|
+
return (
|
|
71
|
+
entryType === 'notification' || entryType === 'task_history'
|
|
72
|
+
|| contentType === 'task' || contentType === 'task_update'
|
|
73
|
+
|| Boolean(taskData.state || taskData.proposal || taskData.parentTaskId || taskData.executorId || taskData.agentId || taskData.payerId || taskData.createdBy)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isRelevantForAgent(ids: (string | null | undefined)[], ownAgentId: string | null): boolean {
|
|
78
|
+
if (!ownAgentId) return true;
|
|
79
|
+
return ids.includes(ownAgentId) || ids.includes(OPEN_AGREEMENT_TARGET);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function normalizeIncomingEvent({ text, metadata = {}, ownAgentId = null }: NormalizeArgs): NormalizedEvent {
|
|
83
|
+
const taskData = metadata.task || null;
|
|
84
|
+
const chatId = normalizeChatId(metadata);
|
|
85
|
+
const receiverId = normalizeReceiverId(metadata, taskData);
|
|
86
|
+
const senderType = metadata.sender?.type ? String(metadata.sender.type).toUpperCase() : undefined;
|
|
87
|
+
|
|
88
|
+
const base = {
|
|
89
|
+
senderId: metadata.sender?.id,
|
|
90
|
+
senderType,
|
|
91
|
+
receiverId: receiverId || undefined,
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (isTaskRelated(metadata, taskData)) {
|
|
96
|
+
const receiverFromMeta = metadata.receiver?.id || metadata.receiverId || null;
|
|
97
|
+
const parties = taskData!.agreement?.parties ?? {};
|
|
98
|
+
const candidateIds = [
|
|
99
|
+
parties.creatorId, parties.providerId, parties.payerId, parties.proposedToId,
|
|
100
|
+
taskData!.agentId, taskData!.executorId, taskData!.payerId, taskData!.proposedTo, taskData!.createdBy,
|
|
101
|
+
receiverFromMeta,
|
|
102
|
+
].filter(Boolean);
|
|
103
|
+
const relevant = isRelevantForAgent(candidateIds, ownAgentId);
|
|
104
|
+
return {
|
|
105
|
+
chatId,
|
|
106
|
+
event: { ...base, type: 'task_result', result: taskData, text },
|
|
107
|
+
shouldProcess: relevant,
|
|
108
|
+
reason: relevant ? null : 'task_not_targeted_to_agent',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const relevant = !ownAgentId || !receiverId || receiverId === ownAgentId || receiverId === OPEN_AGREEMENT_TARGET;
|
|
113
|
+
return {
|
|
114
|
+
chatId,
|
|
115
|
+
event: { ...base, type: 'message', text },
|
|
116
|
+
shouldProcess: relevant,
|
|
117
|
+
reason: relevant ? null : 'message_not_targeted_to_agent',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-private long-term memory.
|
|
3
|
+
*
|
|
4
|
+
* This is the SDK's contract; the *storage* is operator-provided. The agent
|
|
5
|
+
* picks the key shape (often `counterparty:<id>`, sometimes `agreement:<id>:notes`,
|
|
6
|
+
* etc.) — the SDK does not enforce one.
|
|
7
|
+
*
|
|
8
|
+
* Reference implementations: `InMemoryStore` (default; non-durable) and
|
|
9
|
+
* `FileMemoryStore` (single JSON file; useful for local dev). Anything
|
|
10
|
+
* production-grade (sqlite, redis, postgres, …) is operator-owned.
|
|
11
|
+
*
|
|
12
|
+
* Aligns with the project's "agents are third-party" framing: an agent's
|
|
13
|
+
* private notes are its own concern, not the backend's.
|
|
14
|
+
*/
|
|
15
|
+
export interface MemoryStore {
|
|
16
|
+
get(key: string): Promise<unknown | undefined>;
|
|
17
|
+
put(key: string, value: unknown): Promise<void>;
|
|
18
|
+
list(prefix: string): Promise<{ key: string; value: unknown }[]>;
|
|
19
|
+
delete?(key: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Non-durable reference impl. Per-process; lost on restart. */
|
|
23
|
+
export class InMemoryStore implements MemoryStore {
|
|
24
|
+
private readonly store = new Map<string, unknown>();
|
|
25
|
+
|
|
26
|
+
async get(key: string): Promise<unknown | undefined> {
|
|
27
|
+
return this.store.get(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async put(key: string, value: unknown): Promise<void> {
|
|
31
|
+
this.store.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async list(prefix: string): Promise<{ key: string; value: unknown }[]> {
|
|
35
|
+
const out: { key: string; value: unknown }[] = [];
|
|
36
|
+
for (const [k, v] of this.store) {
|
|
37
|
+
if (k.startsWith(prefix)) out.push({ key: k, value: v });
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async delete(key: string): Promise<void> {
|
|
43
|
+
this.store.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Single-file JSON store. Reads on startup, writes on every put — fine for
|
|
49
|
+
* local dev and example agents; not for production (no concurrency control,
|
|
50
|
+
* no atomicity). Operators with serious durability needs ship their own
|
|
51
|
+
* `MemoryStore` (sqlite, redis, …) and pass it via `defineAgent({ memory })`.
|
|
52
|
+
*/
|
|
53
|
+
export class FileMemoryStore implements MemoryStore {
|
|
54
|
+
private cache: Record<string, unknown> | null = null;
|
|
55
|
+
private loading: Promise<void> | null = null;
|
|
56
|
+
|
|
57
|
+
constructor(private readonly path: string) {}
|
|
58
|
+
|
|
59
|
+
private async _ensureLoaded(): Promise<void> {
|
|
60
|
+
if (this.cache) return;
|
|
61
|
+
if (!this.loading) {
|
|
62
|
+
this.loading = (async () => {
|
|
63
|
+
try {
|
|
64
|
+
const fs = await import('fs/promises');
|
|
65
|
+
const txt = await fs.readFile(this.path, 'utf-8');
|
|
66
|
+
this.cache = JSON.parse(txt) as Record<string, unknown>;
|
|
67
|
+
} catch {
|
|
68
|
+
this.cache = {};
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
}
|
|
72
|
+
await this.loading;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async _flush(): Promise<void> {
|
|
76
|
+
if (!this.cache) return;
|
|
77
|
+
const fs = await import('fs/promises');
|
|
78
|
+
await fs.writeFile(this.path, JSON.stringify(this.cache, null, 2), 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async get(key: string): Promise<unknown | undefined> {
|
|
82
|
+
await this._ensureLoaded();
|
|
83
|
+
return this.cache![key];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async put(key: string, value: unknown): Promise<void> {
|
|
87
|
+
await this._ensureLoaded();
|
|
88
|
+
this.cache![key] = value;
|
|
89
|
+
await this._flush();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async list(prefix: string): Promise<{ key: string; value: unknown }[]> {
|
|
93
|
+
await this._ensureLoaded();
|
|
94
|
+
return Object.entries(this.cache!)
|
|
95
|
+
.filter(([k]) => k.startsWith(prefix))
|
|
96
|
+
.map(([key, value]) => ({ key, value }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async delete(key: string): Promise<void> {
|
|
100
|
+
await this._ensureLoaded();
|
|
101
|
+
delete this.cache![key];
|
|
102
|
+
await this._flush();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { Ctx, Outcome, Workflow, Identity, EffectHandler } from '../types.js';
|
|
2
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
+
import { applyOutcomeToCtx, outcomeFromEvent, type RawEvent } from './buildOutcome.js';
|
|
4
|
+
import type { runTurn as RunTurnFn } from './runTurn.js';
|
|
5
|
+
import { PromptBuilder } from './PromptBuilder.js';
|
|
6
|
+
import type { MemoryStore } from '../memory/MemoryStore.js';
|
|
7
|
+
import type { ToolManager } from '../tools/ToolManager.js';
|
|
8
|
+
|
|
9
|
+
export interface MachineSnapshot {
|
|
10
|
+
value: string;
|
|
11
|
+
ctx: Ctx;
|
|
12
|
+
status: 'active' | 'stopped';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MachineInput {
|
|
16
|
+
identity: Identity;
|
|
17
|
+
/** Server-provided ctx for an existing session. If omitted, a fresh ctx is built. */
|
|
18
|
+
ctx?: Ctx;
|
|
19
|
+
/** Server-provided state name for an existing session. Defaults to workflow.initial. */
|
|
20
|
+
state?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MachineOptions {
|
|
24
|
+
workflow: Workflow;
|
|
25
|
+
executionCore: typeof RunTurnFn;
|
|
26
|
+
/** Side channel for all I/O. Provided by the Server. */
|
|
27
|
+
eff: EffectHandler;
|
|
28
|
+
/** Pure tool metadata (schemas, flags). Owned by the Agent. */
|
|
29
|
+
toolManager: ToolManager;
|
|
30
|
+
promptBuilder: PromptBuilder;
|
|
31
|
+
/** Agent-private long-term memory. Operator-provided. */
|
|
32
|
+
memory?: MemoryStore;
|
|
33
|
+
input: MachineInput;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface IncomingFrame {
|
|
37
|
+
event?: RawEvent;
|
|
38
|
+
identity?: Identity;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Subscriber = (snapshot: MachineSnapshot) => void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Per-lane interpreter. Holds (current state name, Ctx). Re-entry is the
|
|
45
|
+
* single primitive: every external event and every turn produces exactly
|
|
46
|
+
* one Outcome that re-enters the current state.
|
|
47
|
+
*
|
|
48
|
+
* Parked states stop on entry. Thinking states run an LLM turn → produce an
|
|
49
|
+
* outcome → re-enter. Transitions are evaluated against the new outcome
|
|
50
|
+
* (first matching `when` wins; no `when` is fallthrough).
|
|
51
|
+
*
|
|
52
|
+
* `_running` guards against concurrent turns on the same machine — outer
|
|
53
|
+
* runtime is expected to serialize via EventQueue, but we belt-and-suspenders.
|
|
54
|
+
*/
|
|
55
|
+
export class AgentMachine {
|
|
56
|
+
workflow: Workflow;
|
|
57
|
+
executionCore: typeof RunTurnFn;
|
|
58
|
+
eff: EffectHandler;
|
|
59
|
+
toolManager: ToolManager;
|
|
60
|
+
promptBuilder: PromptBuilder;
|
|
61
|
+
memory?: MemoryStore;
|
|
62
|
+
state: string;
|
|
63
|
+
status: 'active' | 'stopped' = 'active';
|
|
64
|
+
ctx: Ctx;
|
|
65
|
+
_running = false;
|
|
66
|
+
_subscribers = new Set<Subscriber>();
|
|
67
|
+
_prevSnapshot: MachineSnapshot | null = null;
|
|
68
|
+
_pendingFrames: IncomingFrame[] = [];
|
|
69
|
+
_draining = false;
|
|
70
|
+
_llmCalls = 0;
|
|
71
|
+
_tokens = { total: 0, input: 0, output: 0 };
|
|
72
|
+
|
|
73
|
+
get llmCalls(): number { return this._llmCalls; }
|
|
74
|
+
get tokens(): { total: number; input: number; output: number } { return this._tokens; }
|
|
75
|
+
|
|
76
|
+
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input }: MachineOptions) {
|
|
77
|
+
this.workflow = workflow;
|
|
78
|
+
this.executionCore = executionCore;
|
|
79
|
+
this.eff = eff;
|
|
80
|
+
this.toolManager = toolManager;
|
|
81
|
+
this.promptBuilder = promptBuilder;
|
|
82
|
+
this.memory = memory;
|
|
83
|
+
this.state = input.state || workflow.initial;
|
|
84
|
+
this.ctx = input.ctx
|
|
85
|
+
? { ...input.ctx, identity: input.identity }
|
|
86
|
+
: {
|
|
87
|
+
identity: input.identity,
|
|
88
|
+
activeAgreementId: null,
|
|
89
|
+
activeTaskId: null,
|
|
90
|
+
pendingProposalAgreementId: null,
|
|
91
|
+
delegatedAgreementIds: [],
|
|
92
|
+
delegatedTaskIds: [],
|
|
93
|
+
toolResults: [],
|
|
94
|
+
lastOutcome: { kind: 'enter' },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getSnapshot(): MachineSnapshot {
|
|
99
|
+
return { value: this.state, ctx: this.ctx, status: this.status };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Parked = currently in a parked state and not executing a turn. */
|
|
103
|
+
get isParked(): boolean {
|
|
104
|
+
if (this._running) return false;
|
|
105
|
+
const sd = this.workflow.states[this.state];
|
|
106
|
+
return sd?.kind === 'parked';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
subscribe(fn: Subscriber): { unsubscribe: () => void } {
|
|
110
|
+
this._subscribers.add(fn);
|
|
111
|
+
return { unsubscribe: () => this._subscribers.delete(fn) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Process an incoming wire event: convert to outcome → apply to ctx →
|
|
116
|
+
* evaluate current state's transitions → re-enter or stop.
|
|
117
|
+
*
|
|
118
|
+
* Serialized via a single-slot queue: if a turn is already in flight, the
|
|
119
|
+
* frame is held until the running turn settles, then drained. Without this,
|
|
120
|
+
* an inbound event mid-turn re-entered _step with a stale state and a hot
|
|
121
|
+
* `_running=true`, which spawned overlapping turns and re-created the
|
|
122
|
+
* clarifyingDuringApproval feedback loop even after fixing the workflow
|
|
123
|
+
* fall-through (ZIG-181).
|
|
124
|
+
*/
|
|
125
|
+
async send(frame: IncomingFrame): Promise<void> {
|
|
126
|
+
if (this.status !== 'active') return;
|
|
127
|
+
this._pendingFrames.push(frame);
|
|
128
|
+
if (this._draining) return;
|
|
129
|
+
this._draining = true;
|
|
130
|
+
try {
|
|
131
|
+
while (this._pendingFrames.length) {
|
|
132
|
+
const next = this._pendingFrames.shift()!;
|
|
133
|
+
await this._handleFrame(next);
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
this._draining = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async _handleFrame(frame: IncomingFrame): Promise<void> {
|
|
141
|
+
if (this.status !== 'active') return;
|
|
142
|
+
if (frame.identity) this.ctx.identity = frame.identity;
|
|
143
|
+
|
|
144
|
+
const ev = frame.event;
|
|
145
|
+
this._lastIncomingEvent = ev ?? null;
|
|
146
|
+
runtimeLog.debug(
|
|
147
|
+
'FSM',
|
|
148
|
+
`_handleFrame ev.type=${ev?.type} ev.senderId=${ev?.senderId} ev.receiverId=${ev?.receiverId} myId=${this.ctx.identity?.agentId}`,
|
|
149
|
+
);
|
|
150
|
+
const outcome = outcomeFromEvent(ev, this.ctx.identity.agentId);
|
|
151
|
+
|
|
152
|
+
// `outcomeFromEvent` never returns null — it falls back to `{ kind: 'enter' }`
|
|
153
|
+
// when all events in a batch are no-ops (resource_changed, self-echo, etc.).
|
|
154
|
+
// If a real raw event produced `enter`, that means the event carried no
|
|
155
|
+
// meaningful signal for the FSM, so skip it entirely. Without this guard
|
|
156
|
+
// `idle`'s unconditional `{ to: 'understanding' }` transition fires on every
|
|
157
|
+
// resource_changed wake-up, kicking off an LLM turn that sends a duplicate
|
|
158
|
+
// message, which triggers another resource_changed, and so on.
|
|
159
|
+
if (ev && outcome.kind === 'enter') return;
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
outcome.kind === 'subtask-finished' &&
|
|
163
|
+
ev?.type === 'task_result' &&
|
|
164
|
+
ev?.result?.taskId &&
|
|
165
|
+
this.ctx.delegatedTaskIds.includes(ev.result.taskId)
|
|
166
|
+
) {
|
|
167
|
+
// Already correctly classified — no override needed.
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
applyOutcomeToCtx(this.ctx, outcome);
|
|
171
|
+
await this._step(outcome);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async _step(outcome: Outcome, depth = 0): Promise<void> {
|
|
175
|
+
const fromState = this.state;
|
|
176
|
+
const target = this._evaluateTransitions(outcome);
|
|
177
|
+
const o = outcome as Record<string, unknown>;
|
|
178
|
+
runtimeLog.debug(
|
|
179
|
+
'FSM',
|
|
180
|
+
`_step from=${fromState} outcome.kind=${o?.kind} senderId=${o?.senderId ?? '<none>'} myId=${this.ctx.identity?.agentId ?? '<none>'} → target=${target ?? '(stay)'} depth=${depth}`,
|
|
181
|
+
);
|
|
182
|
+
if (target && target !== this.state) {
|
|
183
|
+
this.state = target;
|
|
184
|
+
this._notify();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const sd = this.workflow.states[this.state];
|
|
188
|
+
if (!sd) return;
|
|
189
|
+
if (sd.kind === 'parked') {
|
|
190
|
+
runtimeLog.debug('FSM', `_step parked state=${this.state} — exit`);
|
|
191
|
+
// Notify so runtime detects park and resolves the wait promise.
|
|
192
|
+
this._notify();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Recursion / runaway-turn guard. Without this, a thinking state whose
|
|
197
|
+
// transitions don't match the turn outcome (e.g. `delegating` not
|
|
198
|
+
// catching `tool-result` from an agent_network search) re-enters the
|
|
199
|
+
// same state forever — the LLM keeps re-rolling search/dummy actions
|
|
200
|
+
// until something accidentally matches, burning tokens and emitting
|
|
201
|
+
// duplicate side effects (ZIG-181: ziggs ran `discoverSpecialist` six
|
|
202
|
+
// times before finally invoking `delegate`). 12 is well above any
|
|
203
|
+
// legitimate same-state turn chain we exercise in tests; if we trip
|
|
204
|
+
// it, treat the state as wedged and park.
|
|
205
|
+
if (depth >= 12) {
|
|
206
|
+
runtimeLog.warn(
|
|
207
|
+
'AgentMachine',
|
|
208
|
+
`state=${this.state} runaway turn loop (depth=${depth}); parking`,
|
|
209
|
+
);
|
|
210
|
+
this._notify();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
runtimeLog.debug('FSM', `_step thinking state=${this.state} — run turn`);
|
|
214
|
+
|
|
215
|
+
// Thinking: run a turn, produce an outcome, re-step.
|
|
216
|
+
this._running = true;
|
|
217
|
+
try {
|
|
218
|
+
const { outcome: turnOutcome, toolResults, llmCalls, tokens } = await this.executionCore({
|
|
219
|
+
stateId: this.state,
|
|
220
|
+
statePrompt: sd.prompt,
|
|
221
|
+
actions: sd.actions,
|
|
222
|
+
incomingEvent: this.ctx.lastOutcome ? this._lastIncomingEvent : null,
|
|
223
|
+
ctx: this.ctx,
|
|
224
|
+
eff: this.eff,
|
|
225
|
+
toolManager: this.toolManager,
|
|
226
|
+
promptBuilder: this.promptBuilder,
|
|
227
|
+
memory: this.memory,
|
|
228
|
+
definition: this.workflow,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.ctx.toolResults = toolResults;
|
|
232
|
+
this._llmCalls += llmCalls;
|
|
233
|
+
this._tokens.total += tokens.total;
|
|
234
|
+
this._tokens.input += tokens.input;
|
|
235
|
+
this._tokens.output += tokens.output;
|
|
236
|
+
applyOutcomeToCtx(this.ctx, turnOutcome);
|
|
237
|
+
this._running = false;
|
|
238
|
+
this._notify();
|
|
239
|
+
|
|
240
|
+
await this._step(turnOutcome, depth + 1);
|
|
241
|
+
} catch (error: unknown) {
|
|
242
|
+
runtimeLog.error('AgentMachine', `state=${this.state} error: ${(error as Error)?.message || error}`);
|
|
243
|
+
const errOutcome: Outcome = {
|
|
244
|
+
kind: 'error',
|
|
245
|
+
source: 'unknown',
|
|
246
|
+
cause: (error as Error)?.message || String(error),
|
|
247
|
+
retryable: false,
|
|
248
|
+
};
|
|
249
|
+
applyOutcomeToCtx(this.ctx, errOutcome);
|
|
250
|
+
this._running = false;
|
|
251
|
+
this._notify();
|
|
252
|
+
// Try to fall through using current state's transitions; if none match
|
|
253
|
+
// the error outcome the machine just parks (next event will re-enter).
|
|
254
|
+
const fallback = this._evaluateTransitions(errOutcome);
|
|
255
|
+
if (fallback && fallback !== this.state) {
|
|
256
|
+
this.state = fallback;
|
|
257
|
+
this._notify();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_evaluateTransitions(outcome: Outcome): string | null {
|
|
263
|
+
const sd = this.workflow.states[this.state];
|
|
264
|
+
if (!sd?.transitions?.length) {
|
|
265
|
+
runtimeLog.debug('FSM', `evaluate state=${this.state}: NO TRANSITIONS DEFINED`);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
for (let i = 0; i < sd.transitions.length; i++) {
|
|
269
|
+
const rule = sd.transitions[i]!;
|
|
270
|
+
let matched: boolean;
|
|
271
|
+
let err: unknown = null;
|
|
272
|
+
if (!rule.when) {
|
|
273
|
+
matched = true;
|
|
274
|
+
} else {
|
|
275
|
+
try { matched = !!rule.when(outcome, this.ctx); }
|
|
276
|
+
catch (e) { matched = false; err = e; }
|
|
277
|
+
}
|
|
278
|
+
runtimeLog.debug(
|
|
279
|
+
'FSM',
|
|
280
|
+
`evaluate state=${this.state} rule[${i}] to=${rule.to} when=${rule.when ? 'fn' : 'always'} → ${matched}${err ? ` ERR=${(err as Error).message}` : ''}`,
|
|
281
|
+
);
|
|
282
|
+
if (matched) return rule.to;
|
|
283
|
+
}
|
|
284
|
+
runtimeLog.debug('FSM', `evaluate state=${this.state}: NONE matched`);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_notify(): void {
|
|
289
|
+
const snapshot = this.getSnapshot();
|
|
290
|
+
for (const fn of this._subscribers) {
|
|
291
|
+
try { fn(snapshot); } catch {}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Held so PromptBuilder/runTurn can render the original wire event in <event>.
|
|
296
|
+
// Set by the runtime layer when it calls send(); we don't classify it twice.
|
|
297
|
+
_lastIncomingEvent: RawEvent | null = null;
|
|
298
|
+
}
|