@ziggs-ai/agent-sdk 0.1.9 → 0.1.11
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/package.json +3 -2
- package/src/AgentHost.ts +54 -4
- package/src/adapters/AnthropicAdapter.ts +206 -0
- package/src/adapters/McpAdapter.ts +177 -32
- package/src/adapters/OpenAIAdapter.ts +11 -6
- package/src/adapters/index.ts +1 -0
- package/src/agent/Agent.ts +3 -0
- package/src/defineAgent.ts +13 -5
- package/src/index.ts +4 -1
- package/src/pricing/fleetTierByAgentId.gen.ts +53 -1
- package/src/runtime/AgentMachine.ts +50 -12
- package/src/runtime/PromptBuilder.ts +20 -1
- package/src/runtime/buildOutcome.ts +51 -6
- package/src/runtime/kickoff.ts +117 -0
- package/src/runtime/runTurn.ts +173 -56
- package/src/runtime/validateWorkflow.ts +26 -5
- package/src/server/ConnectionPool.ts +56 -5
- package/src/server/StateTimers.ts +85 -0
- package/src/server/ZiggsEffectHandler.ts +10 -1
- package/src/server/tasks/TaskService.ts +13 -2
- package/src/server/tools/index.ts +1 -1
- package/src/server/tools/tier1/protocolRunner.ts +10 -1
- package/src/server/tools/tier1/protocolTools.ts +1 -1
- package/src/server/tools/tier2/paymentTools.ts +2 -25
- package/src/types.ts +56 -7
package/src/agent/Agent.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface TickOutput {
|
|
|
40
40
|
state: string;
|
|
41
41
|
/** Whether the machine settled at a parked state. */
|
|
42
42
|
parked: boolean;
|
|
43
|
+
/** True when a state-timeout outcome matched no transition this tick — the host must NOT re-arm. */
|
|
44
|
+
stateTimeoutUnhandled: boolean;
|
|
43
45
|
/** Accumulated LLM call count and token usage for this tick. */
|
|
44
46
|
metrics: { llmCalls: number; tokens: { total: number; input: number; output: number } };
|
|
45
47
|
}
|
|
@@ -95,6 +97,7 @@ export class Agent {
|
|
|
95
97
|
ctx: machine.ctx,
|
|
96
98
|
state: machine.state,
|
|
97
99
|
parked: machine.isParked,
|
|
100
|
+
stateTimeoutUnhandled: machine.stateTimeoutUnhandled,
|
|
98
101
|
metrics: { llmCalls: machine.llmCalls, tokens: machine.tokens },
|
|
99
102
|
};
|
|
100
103
|
}
|
package/src/defineAgent.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { AgentConfig, DefineAgentInput, Workflow } from './types.js';
|
|
2
2
|
import { validateWorkflow } from './runtime/validateWorkflow.js';
|
|
3
|
+
import { injectKickoffDefault } from './runtime/kickoff.js';
|
|
3
4
|
import { runtimeLog } from './shared/runtimeLog.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Validates the workflow synchronously, resolves merged tools and model, and
|
|
7
|
-
* returns a frozen AgentConfig.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* returns a frozen AgentConfig. Never mutates the input. `wait` defaults remain
|
|
9
|
+
* the author's visible spread of `thinkingDefaults`. ONE default is injected (as
|
|
10
|
+
* new objects): the agreement-kickoff state `kickingOff` plus a last-position
|
|
11
|
+
* transition into it, appended to every state that does not already handle a
|
|
12
|
+
* proposer-side proposal-resolved/approve outcome (runtime/kickoff.ts). The
|
|
13
|
+
* state name 'kickingOff' is reserved; defining it throws.
|
|
10
14
|
*
|
|
11
15
|
* Validation throws WorkflowValidationError on hard problems and logs warnings
|
|
12
16
|
* on soft ones (e.g. missing wait fallthrough).
|
|
@@ -17,12 +21,14 @@ export function defineAgent(input: DefineAgentInput): AgentConfig {
|
|
|
17
21
|
if (!input?.states) throw new Error('[defineAgent] states is required');
|
|
18
22
|
if (!input?.initial) throw new Error('[defineAgent] initial state is required');
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
// Agreement-kickoff default (see runtime/kickoff.ts): injected BEFORE
|
|
25
|
+
// validation so the injected state/transitions are validated too.
|
|
26
|
+
const workflow: Workflow = injectKickoffDefault({
|
|
21
27
|
id: input.agentId,
|
|
22
28
|
description: input.description,
|
|
23
29
|
initial: input.initial,
|
|
24
30
|
states: input.states,
|
|
25
|
-
};
|
|
31
|
+
});
|
|
26
32
|
|
|
27
33
|
const { warnings } = validateWorkflow(workflow);
|
|
28
34
|
for (const w of warnings) runtimeLog.warn('defineAgent', w);
|
|
@@ -34,6 +40,7 @@ export function defineAgent(input: DefineAgentInput): AgentConfig {
|
|
|
34
40
|
|
|
35
41
|
const model =
|
|
36
42
|
input.services?.llm?.model ||
|
|
43
|
+
process.env.LLM_MODEL ||
|
|
37
44
|
process.env.OPENAI_MODEL ||
|
|
38
45
|
'gpt-5.4';
|
|
39
46
|
|
|
@@ -46,6 +53,7 @@ export function defineAgent(input: DefineAgentInput): AgentConfig {
|
|
|
46
53
|
|
|
47
54
|
const config: AgentConfig = {
|
|
48
55
|
openaiKey: process.env.OPENAI_API_KEY,
|
|
56
|
+
anthropicKey: process.env.ANTHROPIC_API_KEY,
|
|
49
57
|
...(operatorKey ? { operatorKey } : {}),
|
|
50
58
|
agentId,
|
|
51
59
|
description,
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export { defineAgent } from './defineAgent.js';
|
|
12
|
-
export { createMcpAgent } from './adapters/McpAdapter.js';
|
|
12
|
+
export { createMcpAgent, resolveZiggsGatewayTarget, wrapGatewayFetch, ZiggsGatewayError } from './adapters/McpAdapter.js';
|
|
13
13
|
export type { CreateMcpAgentOptions, McpAgentHandle, McpTransportConfig } from './adapters/McpAdapter.js';
|
|
14
14
|
export { AgentHost, createAgent, createAgentPool } from './AgentHost.js';
|
|
15
15
|
export { Agent } from './agent/Agent.js';
|
|
@@ -18,6 +18,8 @@ export type { SessionResolver, AgentHostOptions } from './AgentHost.js';
|
|
|
18
18
|
export { createZiggsEffectHandler } from './server/ZiggsEffectHandler.js';
|
|
19
19
|
export type { ZiggsEffectDeps } from './server/ZiggsEffectHandler.js';
|
|
20
20
|
export { EventQueue } from './server/EventQueue.js';
|
|
21
|
+
export { StateTimers } from './server/StateTimers.js';
|
|
22
|
+
export type { StateTimeoutEvent } from './server/StateTimers.js';
|
|
21
23
|
export { OutboxBuffer } from './server/OutboxBuffer.js';
|
|
22
24
|
export { normalizeIncomingEvent } from './ingress/normalizeIncoming.js';
|
|
23
25
|
|
|
@@ -26,6 +28,7 @@ export { AgentMachine, type TransitionEvent } from './runtime/AgentMachine.js';
|
|
|
26
28
|
export { runTurn } from './runtime/runTurn.js';
|
|
27
29
|
export { PromptBuilder } from './runtime/PromptBuilder.js';
|
|
28
30
|
export { thinkingDefaults, defineThinkingState, DEFAULT_WAIT_PROMPT } from './runtime/defaults.js';
|
|
31
|
+
export { KICKOFF_STATE, injectKickoffDefault, buildKickoffState } from './runtime/kickoff.js';
|
|
29
32
|
export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
|
|
30
33
|
export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
|
|
31
34
|
|
|
@@ -17,6 +17,13 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
17
17
|
"shopper-agent": "D",
|
|
18
18
|
"hire-agent": "B",
|
|
19
19
|
"ziggs-docs-agent": "B",
|
|
20
|
+
"relay-coordinator": "D",
|
|
21
|
+
"relay-intake-worker": "D",
|
|
22
|
+
"relay-work-worker": "D",
|
|
23
|
+
"relay-deliver-worker": "D",
|
|
24
|
+
"retry-coordinator": "D",
|
|
25
|
+
"retry-fail-worker": "D",
|
|
26
|
+
"retry-ok-worker": "D",
|
|
20
27
|
"weather-forecaster": "A",
|
|
21
28
|
"climate-data-analyst": "A",
|
|
22
29
|
"air-quality-monitor": "A",
|
|
@@ -25,6 +32,8 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
25
32
|
"uv-index-advisor": "A",
|
|
26
33
|
"pollen-tracker": "A",
|
|
27
34
|
"marine-weather-reporter": "A",
|
|
35
|
+
"us-alerts-monitor": "A",
|
|
36
|
+
"daylight-tracker": "A",
|
|
28
37
|
"stock-market-analyst": "A",
|
|
29
38
|
"crypto-tracker": "A",
|
|
30
39
|
"forex-monitor": "A",
|
|
@@ -40,12 +49,16 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
40
49
|
"world-news-briefer": "A",
|
|
41
50
|
"business-news-analyst": "A",
|
|
42
51
|
"reddit-trend-spotter": "A",
|
|
52
|
+
"tech-news-digest": "A",
|
|
53
|
+
"news-searcher": "A",
|
|
54
|
+
"subreddit-pulse": "A",
|
|
43
55
|
"science-news-reporter": "A",
|
|
44
56
|
"sports-score-tracker": "A",
|
|
45
57
|
"breaking-news-alert": "A",
|
|
46
58
|
"wikipedia-researcher": "A",
|
|
47
59
|
"book-finder": "A",
|
|
48
60
|
"dictionary-thesaurus": "A",
|
|
61
|
+
"wikidata-explorer": "A",
|
|
49
62
|
"trivia-master": "A",
|
|
50
63
|
"poetry-curator": "A",
|
|
51
64
|
"quote-finder": "A",
|
|
@@ -75,6 +88,8 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
75
88
|
"distance-calculator": "A",
|
|
76
89
|
"place-finder": "A",
|
|
77
90
|
"ip-geolocator": "A",
|
|
91
|
+
"postal-code-resolver": "A",
|
|
92
|
+
"brewery-finder": "A",
|
|
78
93
|
"astronomy-photo-curator": "A",
|
|
79
94
|
"mars-rover-explorer": "A",
|
|
80
95
|
"neo-tracker": "A",
|
|
@@ -83,9 +98,16 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
83
98
|
"exoplanet-explorer": "A",
|
|
84
99
|
"drug-interaction-checker": "A",
|
|
85
100
|
"solar-system-explorer": "A",
|
|
101
|
+
"species-lookup": "A",
|
|
102
|
+
"census-analyst": "A",
|
|
103
|
+
"river-monitor": "A",
|
|
104
|
+
"fbi-wanted-monitor": "A",
|
|
86
105
|
"github-repo-analyst": "A",
|
|
87
106
|
"github-issue-tracker": "A",
|
|
88
107
|
"github-pr-reviewer": "A",
|
|
108
|
+
"package-inspector": "A",
|
|
109
|
+
"service-status-checker": "A",
|
|
110
|
+
"password-safety-checker": "A",
|
|
89
111
|
"ip-analyst": "A",
|
|
90
112
|
"qr-code-generator": "A",
|
|
91
113
|
"http-debugger": "A",
|
|
@@ -117,6 +139,9 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
117
139
|
"uuid-generator": "A",
|
|
118
140
|
"hash-calculator": "A",
|
|
119
141
|
"base64-encoder": "A",
|
|
142
|
+
"card-dealer": "A",
|
|
143
|
+
"world-clock": "A",
|
|
144
|
+
"link-unfurler": "A",
|
|
120
145
|
"code-reviewer": "B",
|
|
121
146
|
"bug-finder": "B",
|
|
122
147
|
"refactoring-advisor": "B",
|
|
@@ -141,6 +166,30 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
141
166
|
"stack-trace-analyzer": "B",
|
|
142
167
|
"commit-message-writer": "B",
|
|
143
168
|
"schema-designer": "B",
|
|
169
|
+
"name-brainstormer": "B",
|
|
170
|
+
"term-explainer": "B",
|
|
171
|
+
"pros-cons-lister": "B",
|
|
172
|
+
"decision-matrix-builder": "B",
|
|
173
|
+
"checklist-generator": "B",
|
|
174
|
+
"config-converter": "B",
|
|
175
|
+
"csv-to-table": "B",
|
|
176
|
+
"markup-converter": "B",
|
|
177
|
+
"case-converter": "B",
|
|
178
|
+
"color-converter": "B",
|
|
179
|
+
"timestamp-humanizer": "B",
|
|
180
|
+
"text-summarizer": "B",
|
|
181
|
+
"notes-cleaner": "B",
|
|
182
|
+
"audience-rewriter": "B",
|
|
183
|
+
"text-reformatter": "B",
|
|
184
|
+
"json-fixer": "B",
|
|
185
|
+
"sql-explainer": "B",
|
|
186
|
+
"regex-writer": "B",
|
|
187
|
+
"cron-explainer": "B",
|
|
188
|
+
"curl-converter": "B",
|
|
189
|
+
"changelog-writer": "B",
|
|
190
|
+
"stacktrace-explainer": "B",
|
|
191
|
+
"diff-explainer": "B",
|
|
192
|
+
"scaffold-generator": "B",
|
|
144
193
|
"copywriter": "B",
|
|
145
194
|
"technical-writer": "B",
|
|
146
195
|
"blog-post-drafter": "B",
|
|
@@ -270,6 +319,9 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
270
319
|
"feature-prioritizer": "C",
|
|
271
320
|
"threat-model-committee": "C",
|
|
272
321
|
"negotiation-simulator": "C",
|
|
322
|
+
"trip-desk": "C",
|
|
323
|
+
"tech-radar-brief": "C",
|
|
324
|
+
"market-open-brief": "C",
|
|
273
325
|
"aristotle": "B",
|
|
274
326
|
"plato": "B",
|
|
275
327
|
"confucius": "B",
|
|
@@ -720,4 +772,4 @@ export const FLEET_TIER_BY_AGENT_ID: Record<string, FleetPricingTier> = {
|
|
|
720
772
|
"death-studies-scholar": "B"
|
|
721
773
|
} as Record<string, FleetPricingTier>;
|
|
722
774
|
|
|
723
|
-
export const FLEET_TIER_MAP_SIZE =
|
|
775
|
+
export const FLEET_TIER_MAP_SIZE = 765;
|
|
@@ -16,7 +16,8 @@ export type TransitionEvent =
|
|
|
16
16
|
| { kind: 'transition'; fromState: string; toState: string; outcomeKind: string; depth: number }
|
|
17
17
|
| { kind: 'no-match'; state: string; outcomeKind: string; depth: number }
|
|
18
18
|
| { kind: 'depth-guard'; state: string; depth: number }
|
|
19
|
-
| { kind: 'error-escape'; fromState: string; toState: string }
|
|
19
|
+
| { kind: 'error-escape'; fromState: string; toState: string }
|
|
20
|
+
| { kind: 'timeout-unhandled'; state: string; waitedMs: number };
|
|
20
21
|
|
|
21
22
|
interface MachineInput {
|
|
22
23
|
identity: Identity;
|
|
@@ -78,10 +79,12 @@ export class AgentMachine {
|
|
|
78
79
|
_llmCalls = 0;
|
|
79
80
|
_tokens = { total: 0, input: 0, output: 0 };
|
|
80
81
|
_lastUserSenderId: string | null = null;
|
|
82
|
+
_stateTimeoutUnhandled = false;
|
|
81
83
|
_transitionTap: ((event: TransitionEvent) => void) | undefined;
|
|
82
84
|
|
|
83
85
|
get llmCalls(): number { return this._llmCalls; }
|
|
84
86
|
get tokens(): { total: number; input: number; output: number } { return this._tokens; }
|
|
87
|
+
get stateTimeoutUnhandled(): boolean { return this._stateTimeoutUnhandled; }
|
|
85
88
|
|
|
86
89
|
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap }: MachineOptions) {
|
|
87
90
|
this.workflow = workflow;
|
|
@@ -93,14 +96,23 @@ export class AgentMachine {
|
|
|
93
96
|
this._transitionTap = transitionTap;
|
|
94
97
|
this.state = input.state || workflow.initial;
|
|
95
98
|
this.ctx = input.ctx
|
|
96
|
-
? {
|
|
99
|
+
? {
|
|
100
|
+
...input.ctx,
|
|
101
|
+
identity: input.identity,
|
|
102
|
+
// Sessions persisted before pendingSubtasks/collectedResults existed
|
|
103
|
+
// (DynamoDB rows live up to SESSION_TTL_SECONDS = 24h) restore without
|
|
104
|
+
// these fields. Default them ONCE here — nowhere else.
|
|
105
|
+
pendingSubtasks: input.ctx.pendingSubtasks ?? {},
|
|
106
|
+
collectedResults: input.ctx.collectedResults ?? [],
|
|
107
|
+
}
|
|
97
108
|
: {
|
|
98
109
|
identity: input.identity,
|
|
99
110
|
activeAgreementId: null,
|
|
100
111
|
activeTaskId: null,
|
|
101
112
|
pendingProposalAgreementId: null,
|
|
102
113
|
delegatedAgreementIds: [],
|
|
103
|
-
|
|
114
|
+
pendingSubtasks: {},
|
|
115
|
+
collectedResults: [],
|
|
104
116
|
toolResults: [],
|
|
105
117
|
lastOutcome: { kind: 'enter' },
|
|
106
118
|
};
|
|
@@ -172,13 +184,18 @@ export class AgentMachine {
|
|
|
172
184
|
// message, which triggers another resource_changed, and so on.
|
|
173
185
|
if (ev && outcome.kind === 'enter') return;
|
|
174
186
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
// Proposer detection must read pendingProposalAgreementId BEFORE
|
|
188
|
+
// applyOutcomeToCtx clears it on resolution (buildOutcome.ts). Wire frames
|
|
189
|
+
// carry no trustworthy proposer field — agreement_lifecycle drops `parties`
|
|
190
|
+
// in normalizeIncomingEvent, and parties.creator is a principal id, not an
|
|
191
|
+
// agentId (backend ZIG-272) — so the SDK's own "I made this proposal"
|
|
192
|
+
// memory is the signal. Side effect we rely on: the first resolution clears
|
|
193
|
+
// the pending id, so duplicate approval frames compute proposedByMe=false
|
|
194
|
+
// and can never double-kickoff.
|
|
195
|
+
if (outcome.kind === 'proposal-resolved') {
|
|
196
|
+
outcome.proposedByMe =
|
|
197
|
+
this.ctx.pendingProposalAgreementId !== null &&
|
|
198
|
+
outcome.proposal.agreementId === this.ctx.pendingProposalAgreementId;
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
applyOutcomeToCtx(this.ctx, outcome);
|
|
@@ -199,6 +216,24 @@ export class AgentMachine {
|
|
|
199
216
|
this._notify();
|
|
200
217
|
}
|
|
201
218
|
|
|
219
|
+
// A state-timeout only enters at depth 0 (host-armed frame via _handleFrame;
|
|
220
|
+
// turns cannot produce it — validateWorkflow rejects produces:'state-timeout').
|
|
221
|
+
// No matching transition = the author declared timeoutMs but no route: park
|
|
222
|
+
// loudly and tell the host not to re-arm (re-arming would error every
|
|
223
|
+
// timeoutMs forever). Next real event still recovers the lane. A matched
|
|
224
|
+
// transition (target !== null) — including an explicit self-loop — is
|
|
225
|
+
// "handled" and re-arms as a heartbeat.
|
|
226
|
+
if (outcome.kind === 'state-timeout' && depth === 0 && !target) {
|
|
227
|
+
this._stateTimeoutUnhandled = true;
|
|
228
|
+
runtimeLog.error(
|
|
229
|
+
'AgentMachine',
|
|
230
|
+
`state=${this.state} timed out after ${outcome.waitedMs}ms but no transition matches { kind: 'state-timeout' } — parking without a timer. Add a state-timeout transition to "${this.state}" or remove timeoutMs.`,
|
|
231
|
+
);
|
|
232
|
+
this._transitionTap?.({ kind: 'timeout-unhandled', state: this.state, waitedMs: outcome.waitedMs });
|
|
233
|
+
this._notify();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
202
237
|
const sd = this.workflow.states[this.state];
|
|
203
238
|
if (!sd) return;
|
|
204
239
|
if (sd.kind === 'parked') {
|
|
@@ -217,7 +252,7 @@ export class AgentMachine {
|
|
|
217
252
|
fromState === this.state &&
|
|
218
253
|
!target
|
|
219
254
|
) {
|
|
220
|
-
if (this._escapeToInitialParked(outcome, fromState)) return;
|
|
255
|
+
if (await this._escapeToInitialParked(outcome, fromState)) return;
|
|
221
256
|
runtimeLog.warn(
|
|
222
257
|
'AgentMachine',
|
|
223
258
|
`state=${fromState} outcome.kind=${(outcome as Record<string, unknown>).kind} matched no transition; parking (depth=${depth})`,
|
|
@@ -299,7 +334,7 @@ export class AgentMachine {
|
|
|
299
334
|
}
|
|
300
335
|
|
|
301
336
|
/** Error with no author-defined escape: park at workflow.initial if it is parked. */
|
|
302
|
-
_escapeToInitialParked(outcome: Outcome, fromState: string): boolean {
|
|
337
|
+
async _escapeToInitialParked(outcome: Outcome, fromState: string): Promise<boolean> {
|
|
303
338
|
if (outcome.kind !== 'error') return false;
|
|
304
339
|
const initial = this.workflow.initial;
|
|
305
340
|
const initialSd = this.workflow.states[initial];
|
|
@@ -312,6 +347,9 @@ export class AgentMachine {
|
|
|
312
347
|
);
|
|
313
348
|
this._transitionTap?.({ kind: 'error-escape', fromState, toState: initial });
|
|
314
349
|
this.state = initial;
|
|
350
|
+
// The turn failed and no workflow transition handled it — a waiting human
|
|
351
|
+
// got nothing, so apologize before parking (same text as the other parks).
|
|
352
|
+
await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
|
|
315
353
|
this._notify();
|
|
316
354
|
return true;
|
|
317
355
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Action, Ctx, PromptDef } from '../types.js';
|
|
1
|
+
import type { Action, CollectedSubtaskResult, Ctx, PromptDef } from '../types.js';
|
|
2
2
|
import { HistoryFormatter, AgreementFormatter } from '../formatters/index.js';
|
|
3
3
|
import { validateContext } from '../cognition/validateContext.js';
|
|
4
4
|
import type { RawEvent } from './buildOutcome.js';
|
|
@@ -151,6 +151,11 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessage
|
|
|
151
151
|
lines.push('');
|
|
152
152
|
lines.push(this._renderAgreements(agreements));
|
|
153
153
|
|
|
154
|
+
if (ctx?.collectedResults?.length) {
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push(this._renderSubtaskResults(ctx.collectedResults));
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
if (users.length > 0 || otherAgents.length > 0) {
|
|
155
160
|
lines.push('');
|
|
156
161
|
lines.push(this._renderRecipients(users, otherAgents, agreements, agentId));
|
|
@@ -171,10 +176,24 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessage
|
|
|
171
176
|
if (ctx.activeTaskId) out.activeTaskId = ctx.activeTaskId;
|
|
172
177
|
if (ctx.pendingProposalAgreementId) out.pendingProposalAgreementId = ctx.pendingProposalAgreementId;
|
|
173
178
|
if (ctx.delegatedAgreementIds?.length) out.delegatedAgreementIds = ctx.delegatedAgreementIds;
|
|
179
|
+
if (Object.keys(ctx.pendingSubtasks ?? {}).length) out.pendingSubtasks = ctx.pendingSubtasks;
|
|
174
180
|
if (ctx.lastOutcome) out.lastOutcomeKind = ctx.lastOutcome.kind;
|
|
175
181
|
return `<machine_context>\n${JSON.stringify(out, null, 2)}\n</machine_context>`;
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
_renderSubtaskResults(results: CollectedSubtaskResult[]): string {
|
|
185
|
+
const lines: string[] = ['<subtask_results>'];
|
|
186
|
+
lines.push(`All ${results.length} finished subtask(s) for the current engagement, oldest first. Synthesize from ALL of them — not just the most recent event.`);
|
|
187
|
+
for (const r of results) {
|
|
188
|
+
const payload = r.result == null ? null : typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
|
189
|
+
const display = payload && payload.length > 2000 ? payload.slice(0, 2000) + `… [truncated of ${payload.length} chars]` : payload;
|
|
190
|
+
lines.push(`- taskId=${r.taskId} agent=${r.agentId ?? 'unknown'} status=${r.status}`);
|
|
191
|
+
lines.push(` result: ${display ?? '(none)'}`);
|
|
192
|
+
}
|
|
193
|
+
lines.push('</subtask_results>');
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
178
197
|
_renderEvent(event: RawEvent | AnyObj | null, senderMeta: SenderMeta): string {
|
|
179
198
|
if (!event) return '<event type="none">No incoming event.</event>';
|
|
180
199
|
|
|
@@ -97,6 +97,7 @@ function singleEventToOutcome(ev: RawEvent, ownAgentId: string | null): Outcome
|
|
|
97
97
|
kind: 'proposal-resolved',
|
|
98
98
|
action,
|
|
99
99
|
proposal: { agreementId },
|
|
100
|
+
proposedByMe: false, // real value stamped in AgentMachine._handleFrame from ctx (needs pendingProposalAgreementId, unavailable here)
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
return null;
|
|
@@ -124,7 +125,7 @@ function singleEventToOutcome(ev: RawEvent, ownAgentId: string | null): Outcome
|
|
|
124
125
|
const proposal: ProposalRef = agId
|
|
125
126
|
? { agreementId: agId, agreement: result as unknown as AgreementRef }
|
|
126
127
|
: { agreementId: '' };
|
|
127
|
-
return { kind: 'proposal-resolved', action: proposalAction as 'approve' | 'reject', proposal };
|
|
128
|
+
return { kind: 'proposal-resolved', action: proposalAction as 'approve' | 'reject', proposal, proposedByMe: false }; // real value stamped in AgentMachine._handleFrame from ctx
|
|
128
129
|
}
|
|
129
130
|
// Other lifecycle ops (countered, expired) treated as enter — no FSM action needed here.
|
|
130
131
|
return null;
|
|
@@ -194,6 +195,7 @@ function singleEventToOutcome(ev: RawEvent, ownAgentId: string | null): Outcome
|
|
|
194
195
|
kind: 'proposal-resolved',
|
|
195
196
|
action: reason === 'proposal_approved' ? 'approve' : 'reject',
|
|
196
197
|
proposal,
|
|
198
|
+
proposedByMe: false, // real value stamped in AgentMachine._handleFrame from ctx
|
|
197
199
|
};
|
|
198
200
|
}
|
|
199
201
|
if (resource.kind === 'task-state' && reason === 'subtask_completed') {
|
|
@@ -204,6 +206,11 @@ function singleEventToOutcome(ev: RawEvent, ownAgentId: string | null): Outcome
|
|
|
204
206
|
return null;
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
if (ev.type === 'state-timeout') {
|
|
210
|
+
// Synthetic frame from AgentHost's StateTimers — host-constructed, fields always present.
|
|
211
|
+
return { kind: 'state-timeout', state: ev.state as string, waitedMs: ev.waitedMs as number };
|
|
212
|
+
}
|
|
213
|
+
|
|
207
214
|
// Only real inbound chat messages should drive the FSM. Backend bookkeeping
|
|
208
215
|
// (presence/heartbeat, raw `batch` outer frames, etc.) are NOT chat traffic.
|
|
209
216
|
if (ev.type !== 'message') return null;
|
|
@@ -223,6 +230,10 @@ function outcomeRank(o: Outcome): number {
|
|
|
223
230
|
case 'proposal-resolved': return 90;
|
|
224
231
|
case 'subtask-finished': return 80;
|
|
225
232
|
case 'message-received': return 10;
|
|
233
|
+
// Any real event coalesced into the same batch supersedes the timer —
|
|
234
|
+
// waking to "nothing happened" is moot when something did. Must stay below
|
|
235
|
+
// message-received (10).
|
|
236
|
+
case 'state-timeout': return 1;
|
|
226
237
|
default: return 50;
|
|
227
238
|
}
|
|
228
239
|
}
|
|
@@ -390,9 +401,13 @@ function buildOutcomeOfKind(
|
|
|
390
401
|
switch (kind) {
|
|
391
402
|
case 'enter':
|
|
392
403
|
case 'wait':
|
|
393
|
-
case 'timeout':
|
|
394
404
|
return { kind };
|
|
395
405
|
|
|
406
|
+
case 'state-timeout':
|
|
407
|
+
// Runtime-generated only (validateWorkflow rejects produces:'state-timeout').
|
|
408
|
+
// Break-fast backstop if an action somehow declares it.
|
|
409
|
+
throw new Error(`action produced reserved outcome kind "state-timeout" — runtime-generated only`);
|
|
410
|
+
|
|
396
411
|
case 'message-sent':
|
|
397
412
|
return { kind, text: primary?.message, receiverId: primary?.receiverId };
|
|
398
413
|
|
|
@@ -431,7 +446,7 @@ function buildOutcomeOfKind(
|
|
|
431
446
|
const proposal: ProposalRef = ag
|
|
432
447
|
? { agreementId: ag.agreementId, agreement: ag }
|
|
433
448
|
: { agreementId: result?.agreementId || '' };
|
|
434
|
-
return { kind, action: action_, proposal };
|
|
449
|
+
return { kind, action: action_, proposal, proposedByMe: false }; // turn path = our own agreement_respond ran — we are the responder, by definition not the proposer
|
|
435
450
|
}
|
|
436
451
|
case 'tool-result':
|
|
437
452
|
return { kind, tool, result };
|
|
@@ -471,7 +486,12 @@ export function applyOutcomeToCtx(ctx: Ctx, outcome: Outcome): void {
|
|
|
471
486
|
const t = outcome.task || ({} as TaskRef);
|
|
472
487
|
const tid = t.taskId;
|
|
473
488
|
const agId = t.agreementId || t.agreement?.agreementId;
|
|
474
|
-
if (tid && !
|
|
489
|
+
if (tid && !(tid in ctx.pendingSubtasks)) {
|
|
490
|
+
ctx.pendingSubtasks[tid] = {
|
|
491
|
+
agentId: t.assigneeId ?? t.executorId ?? t.agreement?.parties?.provider ?? null,
|
|
492
|
+
spawnedAt: Date.now(),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
475
495
|
if (agId && !ctx.delegatedAgreementIds.includes(agId)) ctx.delegatedAgreementIds.push(agId);
|
|
476
496
|
return;
|
|
477
497
|
}
|
|
@@ -500,7 +520,31 @@ export function applyOutcomeToCtx(ctx: Ctx, outcome: Outcome): void {
|
|
|
500
520
|
const t = outcome.task;
|
|
501
521
|
const taskId = t?.taskId;
|
|
502
522
|
const agId = t?.agreementId || t?.agreement?.agreementId;
|
|
503
|
-
if (taskId)
|
|
523
|
+
if (taskId) {
|
|
524
|
+
const pending = ctx.pendingSubtasks[taskId];
|
|
525
|
+
delete ctx.pendingSubtasks[taskId];
|
|
526
|
+
const payload = t?.result ?? null;
|
|
527
|
+
const existing = ctx.collectedResults.find(r => r.taskId === taskId);
|
|
528
|
+
if (!existing) {
|
|
529
|
+
ctx.collectedResults.push({
|
|
530
|
+
taskId,
|
|
531
|
+
agentId: pending?.agentId ?? t?.assigneeId ?? t?.executorId ?? t?.agreement?.parties?.provider ?? null,
|
|
532
|
+
status: outcome.status,
|
|
533
|
+
result: payload,
|
|
534
|
+
});
|
|
535
|
+
} else if (existing.result == null && payload != null) {
|
|
536
|
+
// A bare resource_changed(subtask_completed) skeleton ({taskId, state})
|
|
537
|
+
// can arrive BEFORE the full task_result for the same completion.
|
|
538
|
+
// Upgrade the placeholder in place instead of dropping the real payload.
|
|
539
|
+
// The skeleton often lacks executor identity, so also fill agentId
|
|
540
|
+
// from the fuller frame when the placeholder recorded none.
|
|
541
|
+
existing.status = outcome.status;
|
|
542
|
+
existing.result = payload;
|
|
543
|
+
if (existing.agentId == null) {
|
|
544
|
+
existing.agentId = t?.assigneeId ?? t?.executorId ?? t?.agreement?.parties?.provider ?? null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
504
548
|
if (agId) ctx.delegatedAgreementIds = ctx.delegatedAgreementIds.filter(id => id !== agId);
|
|
505
549
|
return;
|
|
506
550
|
}
|
|
@@ -508,7 +552,8 @@ export function applyOutcomeToCtx(ctx: Ctx, outcome: Outcome): void {
|
|
|
508
552
|
ctx.activeAgreementId = null;
|
|
509
553
|
ctx.activeTaskId = null;
|
|
510
554
|
ctx.pendingProposalAgreementId = null;
|
|
511
|
-
ctx.
|
|
555
|
+
ctx.pendingSubtasks = {};
|
|
556
|
+
ctx.collectedResults = [];
|
|
512
557
|
ctx.delegatedAgreementIds = [];
|
|
513
558
|
return;
|
|
514
559
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Outcome, State, ThinkingState, Transition, Workflow } from '../types.js';
|
|
2
|
+
import { thinkingDefaults } from './defaults.js';
|
|
3
|
+
import { PROBE_CTX } from './validateWorkflow.js';
|
|
4
|
+
|
|
5
|
+
/** Reserved state name for the injected agreement-kickoff default. */
|
|
6
|
+
export const KICKOFF_STATE = 'kickingOff';
|
|
7
|
+
|
|
8
|
+
// Synthetic outcome used to ask "does this state already handle a proposer-side
|
|
9
|
+
// approval?" — the same probe pattern validateWorkflow uses for the wait/error
|
|
10
|
+
// fallthrough checks.
|
|
11
|
+
const PROBE_APPROVE: Outcome = {
|
|
12
|
+
kind: 'proposal-resolved',
|
|
13
|
+
action: 'approve',
|
|
14
|
+
proposal: { agreementId: '__kickoff_probe__' },
|
|
15
|
+
proposedByMe: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const KICKOFF_TRANSITION: Transition = {
|
|
19
|
+
to: KICKOFF_STATE,
|
|
20
|
+
when: (o, ctx) =>
|
|
21
|
+
o.kind === 'proposal-resolved' &&
|
|
22
|
+
o.action === 'approve' &&
|
|
23
|
+
o.proposedByMe === true &&
|
|
24
|
+
// "first task" guard — both branches cover one exact scenario: a task under
|
|
25
|
+
// THIS agreement already exists because a racing task_result frame was
|
|
26
|
+
// applied to ctx before this approval frame.
|
|
27
|
+
// (1) we already delegated under it:
|
|
28
|
+
!ctx.delegatedAgreementIds.includes(o.proposal.agreementId) &&
|
|
29
|
+
// (2) we already hold an assigned task bound to it (task-assigned sets
|
|
30
|
+
// activeTaskId + activeAgreementId together in applyOutcomeToCtx):
|
|
31
|
+
!(ctx.activeTaskId !== null && ctx.activeAgreementId === o.proposal.agreementId),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// A state "handles" proposer-approval if some transition would fire for it.
|
|
35
|
+
// An unconditional (when-less) transition counts: it catches every outcome,
|
|
36
|
+
// so the kickoff transition — appended LAST — would be unreachable dead code
|
|
37
|
+
// there anyway. Consequence to know: a state whose only route for a
|
|
38
|
+
// proposal-resolved/approve is an unconditional fallthrough to an UNRELATED
|
|
39
|
+
// state will NOT auto-kickoff (the approval follows the fallthrough instead).
|
|
40
|
+
// Authors who want guaranteed kickoff in such a state must add an explicit
|
|
41
|
+
// `when: o => o.kind === 'proposal-resolved' && o.action === 'approve'`
|
|
42
|
+
// transition (which this probe then respects) — or route approval into a
|
|
43
|
+
// thinking state that spawns the task itself.
|
|
44
|
+
function stateHandlesProposerApproval(state: State): boolean {
|
|
45
|
+
return state.transitions.some(t => {
|
|
46
|
+
if (!t.when) return true;
|
|
47
|
+
try { return !!t.when(PROBE_APPROVE, PROBE_CTX); }
|
|
48
|
+
catch { return false; }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildKickoffState(initial: string): ThinkingState {
|
|
53
|
+
const dflt = thinkingDefaults({ initial });
|
|
54
|
+
return {
|
|
55
|
+
kind: 'thinking',
|
|
56
|
+
prompt: {
|
|
57
|
+
role: 'The counterparty just approved an agreement you proposed. The platform never auto-starts work — as the proposer, you spawn the first task now.',
|
|
58
|
+
goal: 'Spawn exactly one task under the just-approved agreement so work begins.',
|
|
59
|
+
constraints:
|
|
60
|
+
'The approved agreement id is in <event> and as activeAgreementId in <machine_context>; the agreement row is in <agreements>. Call task_spawn with that agreementId and a description of the concrete work the agreement covers. Include ALL specifics the counterparty provided in <history> (addresses, quantities, times, item names, deliverables) — the executor has no other way to get them. Plain natural language, no step numbering. Do not re-negotiate — the agreement is active. Do not send a chat message instead of spawning. If <agreements> or <history> show a task already running under this agreement, choose wait.',
|
|
61
|
+
},
|
|
62
|
+
actions: {
|
|
63
|
+
...dflt.actions,
|
|
64
|
+
spawnFirstTask: {
|
|
65
|
+
prompt: {
|
|
66
|
+
when: 'Always — this state exists only to spawn the first task under the agreement whose proposal was just approved. Pick wait only if a task already exists under it.',
|
|
67
|
+
instruction: 'Spawn the first task under the approved agreement so work starts.',
|
|
68
|
+
format: '{"agreementId": "<the agreement whose proposal was just approved>", "description": "<concrete work + all counterparty specifics from history>"}',
|
|
69
|
+
},
|
|
70
|
+
tool: 'task_spawn',
|
|
71
|
+
// Function form so a failed task_spawn surfaces as an error outcome
|
|
72
|
+
// (→ dflt error transition + machine warn log) instead of a fake
|
|
73
|
+
// task-delegated with an empty task.
|
|
74
|
+
produces: (result) => {
|
|
75
|
+
const r = result as { taskId?: string; task?: { taskId?: string } } | undefined;
|
|
76
|
+
return r?.taskId || r?.task?.taskId ? 'task-delegated' : 'error';
|
|
77
|
+
},
|
|
78
|
+
terminatesLoop: true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
transitions: [
|
|
82
|
+
{ to: initial, when: o => o.kind === 'task-delegated' },
|
|
83
|
+
...dflt.transitions, // wait / message-sent / error → initial
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Agreement-kickoff default. For every state whose transitions do not already
|
|
90
|
+
* match a proposer-side approval (probe above), append a transition to the
|
|
91
|
+
* injected `kickingOff` state. Appended LAST so any user transition on the same
|
|
92
|
+
* outcome always wins (first match wins in AgentMachine._evaluateTransitions).
|
|
93
|
+
* Returns a NEW workflow — input states are not mutated. After kickoff the
|
|
94
|
+
* machine returns to `initial`; when this agent is itself the provider the
|
|
95
|
+
* backend then assigns the spawned task back to it as a normal task-assigned
|
|
96
|
+
* event, which the author's existing work loop handles.
|
|
97
|
+
*/
|
|
98
|
+
export function injectKickoffDefault(workflow: Workflow): Workflow {
|
|
99
|
+
if (workflow.states[KICKOFF_STATE]) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[defineAgent] state name "${KICKOFF_STATE}" is reserved for the injected agreement-kickoff default — rename the state`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const states: Record<string, State> = {};
|
|
105
|
+
let injected = false;
|
|
106
|
+
for (const [name, state] of Object.entries(workflow.states)) {
|
|
107
|
+
if (stateHandlesProposerApproval(state)) {
|
|
108
|
+
states[name] = state;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
states[name] = { ...state, transitions: [...state.transitions, KICKOFF_TRANSITION] } as State;
|
|
112
|
+
injected = true;
|
|
113
|
+
}
|
|
114
|
+
if (!injected) return workflow; // every state handles approval — nothing to add, no dead state
|
|
115
|
+
states[KICKOFF_STATE] = buildKickoffState(workflow.initial);
|
|
116
|
+
return { ...workflow, states };
|
|
117
|
+
}
|