@hanfani/core 0.1.1
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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/conformance.d.ts +25 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +90 -0
- package/dist/conformance.js.map +1 -0
- package/dist/defineAgent.d.ts +76 -0
- package/dist/defineAgent.d.ts.map +1 -0
- package/dist/defineAgent.js +59 -0
- package/dist/defineAgent.js.map +1 -0
- package/dist/definePrompt.d.ts +15 -0
- package/dist/definePrompt.d.ts.map +1 -0
- package/dist/definePrompt.js +23 -0
- package/dist/definePrompt.js.map +1 -0
- package/dist/defineWorkflow.d.ts +10 -0
- package/dist/defineWorkflow.d.ts.map +1 -0
- package/dist/defineWorkflow.js +38 -0
- package/dist/defineWorkflow.js.map +1 -0
- package/dist/delivery.d.ts +8 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +21 -0
- package/dist/delivery.js.map +1 -0
- package/dist/fold.d.ts +8 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/fold.js +98 -0
- package/dist/fold.js.map +1 -0
- package/dist/gate.d.ts +25 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +22 -0
- package/dist/gate.js.map +1 -0
- package/dist/handoff.d.ts +77 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +52 -0
- package/dist/handoff.js.map +1 -0
- package/dist/handoffNote.d.ts +11 -0
- package/dist/handoffNote.d.ts.map +1 -0
- package/dist/handoffNote.js +6 -0
- package/dist/handoffNote.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +9 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +13 -0
- package/dist/integration.js.map +1 -0
- package/dist/integrationAuth.d.ts +8 -0
- package/dist/integrationAuth.d.ts.map +1 -0
- package/dist/integrationAuth.js +5 -0
- package/dist/integrationAuth.js.map +1 -0
- package/dist/lifecycle.d.ts +16 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +57 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/lifecycleNote.d.ts +13 -0
- package/dist/lifecycleNote.d.ts.map +1 -0
- package/dist/lifecycleNote.js +17 -0
- package/dist/lifecycleNote.js.map +1 -0
- package/dist/messages.d.ts +21 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +78 -0
- package/dist/messages.js.map +1 -0
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +6 -0
- package/dist/prompt.js.map +1 -0
- package/dist/providers.d.ts +4 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +12 -0
- package/dist/providers.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/conformance.ts +118 -0
- package/src/defineAgent.ts +63 -0
- package/src/definePrompt.ts +33 -0
- package/src/defineWorkflow.ts +52 -0
- package/src/delivery.ts +28 -0
- package/src/fold.ts +129 -0
- package/src/gate.ts +26 -0
- package/src/handoff.ts +58 -0
- package/src/handoffNote.ts +15 -0
- package/src/index.ts +101 -0
- package/src/integration.ts +14 -0
- package/src/integrationAuth.ts +8 -0
- package/src/lifecycle.ts +61 -0
- package/src/lifecycleNote.ts +27 -0
- package/src/messages.ts +99 -0
- package/src/prompt.ts +8 -0
- package/src/providers.ts +12 -0
- package/src/types.ts +151 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { BaseEvent, Message, RunAgentInput, ToolCall } from '@ag-ui/client';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type { AgentDefinition } from './defineAgent.js';
|
|
4
|
+
export type { Message, ToolCall };
|
|
5
|
+
/** Lifecycle phase of a work item. */
|
|
6
|
+
export type Phase = 'queued' | 'active' | 'awaiting_human' | 'terminal';
|
|
7
|
+
/** Terminal (and running) outcome of a work item. */
|
|
8
|
+
export type Outcome = 'running' | 'done' | 'stopped' | 'rejected' | 'error' | 'superseded' | 'reset' | 'dismissed';
|
|
9
|
+
/** Whether an agent accepts external input or only runs as a downstream worker. */
|
|
10
|
+
export type AgentRole = 'input' | 'worker';
|
|
11
|
+
/** Result of a health probe. */
|
|
12
|
+
export type HealthCheck = {
|
|
13
|
+
ok: true;
|
|
14
|
+
detail?: string;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
error: string;
|
|
18
|
+
hint: string;
|
|
19
|
+
};
|
|
20
|
+
/** How an integration authenticates. */
|
|
21
|
+
export type AuthSpec = {
|
|
22
|
+
kind: 'none';
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'apiKey';
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'oauth2';
|
|
27
|
+
provider: string;
|
|
28
|
+
scopes: string[];
|
|
29
|
+
} | {
|
|
30
|
+
kind: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
/** A credential resolved for a given integration + connection. */
|
|
34
|
+
export type ResolvedCredential = {
|
|
35
|
+
kind: 'apiKey';
|
|
36
|
+
apiKey: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'oauth2';
|
|
39
|
+
accessToken: string;
|
|
40
|
+
refreshToken?: string;
|
|
41
|
+
expiresAt?: number;
|
|
42
|
+
raw?: unknown;
|
|
43
|
+
} | {
|
|
44
|
+
kind: string;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
export type CredentialResolver = (ctx: {
|
|
48
|
+
integration: string;
|
|
49
|
+
connectionId: string;
|
|
50
|
+
auth: AuthSpec;
|
|
51
|
+
}) => Promise<ResolvedCredential | null>;
|
|
52
|
+
/** What a prompt strategy decides to do when a gate resolves. */
|
|
53
|
+
export type ResumeOutcome = {
|
|
54
|
+
kind: 'prompt';
|
|
55
|
+
text: string;
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'message';
|
|
58
|
+
text: string;
|
|
59
|
+
} | null;
|
|
60
|
+
export interface ResumeHandle {
|
|
61
|
+
runId: string;
|
|
62
|
+
input: RunAgentInput;
|
|
63
|
+
}
|
|
64
|
+
export interface GateResolution {
|
|
65
|
+
gateId: string;
|
|
66
|
+
decision: 'approved' | 'rejected';
|
|
67
|
+
form?: Record<string, unknown>;
|
|
68
|
+
comment?: string;
|
|
69
|
+
executedResult?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
export interface PromptStrategy {
|
|
72
|
+
buildFirst(input: RunAgentInput): string;
|
|
73
|
+
buildResume?(args: Record<string, unknown>, executedResult?: Record<string, unknown>): ResumeOutcome;
|
|
74
|
+
}
|
|
75
|
+
export interface Provider {
|
|
76
|
+
run(input: RunAgentInput): AsyncIterable<BaseEvent>;
|
|
77
|
+
resume?(handle: ResumeHandle, resolution: GateResolution): AsyncIterable<BaseEvent>;
|
|
78
|
+
}
|
|
79
|
+
export interface ProviderConfig {
|
|
80
|
+
approvalNames: readonly string[];
|
|
81
|
+
surfaceTools: readonly string[];
|
|
82
|
+
allowedTools: readonly string[];
|
|
83
|
+
prompts: PromptStrategy;
|
|
84
|
+
instructions: string;
|
|
85
|
+
agentId: string;
|
|
86
|
+
}
|
|
87
|
+
export type ProviderFactory = (config: ProviderConfig) => Provider;
|
|
88
|
+
export interface ProviderRegistry {
|
|
89
|
+
resolve(name: string): ProviderFactory;
|
|
90
|
+
}
|
|
91
|
+
export interface Lifecycle {
|
|
92
|
+
phase: Phase;
|
|
93
|
+
outcome: Outcome;
|
|
94
|
+
isLive: boolean;
|
|
95
|
+
isVisible: boolean;
|
|
96
|
+
covers: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** Where a produced payload should be delivered. */
|
|
99
|
+
export type Destination = {
|
|
100
|
+
kind: 'agent';
|
|
101
|
+
agentId: string;
|
|
102
|
+
} | {
|
|
103
|
+
kind: 'contract';
|
|
104
|
+
workflow: string;
|
|
105
|
+
input: string;
|
|
106
|
+
};
|
|
107
|
+
export type DeliveryResult = {
|
|
108
|
+
ok: true;
|
|
109
|
+
instanceId: string;
|
|
110
|
+
targetWorkflow?: string;
|
|
111
|
+
} | {
|
|
112
|
+
ok: false;
|
|
113
|
+
error: string;
|
|
114
|
+
};
|
|
115
|
+
/** The consequential side effect bound to an approved tool. Runs on the server only. */
|
|
116
|
+
export type EffectFn = (form: Record<string, unknown>, ctx: {
|
|
117
|
+
workItemId: string;
|
|
118
|
+
gateId: string;
|
|
119
|
+
}) => Promise<Record<string, unknown>>;
|
|
120
|
+
export type WorkflowAgent = {
|
|
121
|
+
agent: AgentDefinition;
|
|
122
|
+
role: AgentRole;
|
|
123
|
+
};
|
|
124
|
+
export type WorkflowConnection = {
|
|
125
|
+
integration: string;
|
|
126
|
+
connection?: string;
|
|
127
|
+
provider: string;
|
|
128
|
+
};
|
|
129
|
+
export type WorkflowInput = {
|
|
130
|
+
name: string;
|
|
131
|
+
schema: z.ZodTypeAny;
|
|
132
|
+
agentId: string;
|
|
133
|
+
};
|
|
134
|
+
export type WorkflowDescriptor = {
|
|
135
|
+
id: string;
|
|
136
|
+
label: string;
|
|
137
|
+
iconName: string;
|
|
138
|
+
agents: WorkflowAgent[];
|
|
139
|
+
entryAgentId: string;
|
|
140
|
+
inputs: WorkflowInput[];
|
|
141
|
+
prompt?: string;
|
|
142
|
+
connections?: WorkflowConnection[];
|
|
143
|
+
rerun?: 'refresh' | 'history';
|
|
144
|
+
resetOnStart?: boolean;
|
|
145
|
+
};
|
|
146
|
+
export type ReadResult<T> = T | {
|
|
147
|
+
error: string;
|
|
148
|
+
};
|
|
149
|
+
export type BatchActionResult = {
|
|
150
|
+
done: string[];
|
|
151
|
+
failed: {
|
|
152
|
+
messageId: string;
|
|
153
|
+
error: string;
|
|
154
|
+
}[];
|
|
155
|
+
} | {
|
|
156
|
+
error: string;
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hanfani/core",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Headless engine and types for the Hanfani agent framework.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Aziz Mashkour",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/azizmashkour/hanfani-packages.git",
|
|
25
|
+
"directory": "packages/core"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"hanfani",
|
|
29
|
+
"agent",
|
|
30
|
+
"workflow",
|
|
31
|
+
"ai",
|
|
32
|
+
"human-in-the-loop"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@ag-ui/client": "^0.0.55",
|
|
36
|
+
"zod": "^3.25.76"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.build.json",
|
|
40
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
41
|
+
"test": "vitest run"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { EventType } from '@ag-ui/client'
|
|
2
|
+
import type { BaseEvent, RunAgentInput } from '@ag-ui/client'
|
|
3
|
+
import { readGateOpened, type GateOpenedValue } from './gate.js'
|
|
4
|
+
import type { GateResolution, Provider, ResumeHandle } from './types.js'
|
|
5
|
+
|
|
6
|
+
export interface ConformanceScenario {
|
|
7
|
+
approvalNames: readonly string[]
|
|
8
|
+
surfaceTools: readonly string[]
|
|
9
|
+
turn1Input: RunAgentInput
|
|
10
|
+
approved: { handle: ResumeHandle; resolution: GateResolution }
|
|
11
|
+
rejected: { handle: ResumeHandle; resolution: GateResolution }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ConformanceCheck {
|
|
15
|
+
name: string
|
|
16
|
+
run(makeProvider: () => Provider, scenario: ConformanceScenario): Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function collect(events: AsyncIterable<BaseEvent>): Promise<BaseEvent[]> {
|
|
20
|
+
const out: BaseEvent[] = []
|
|
21
|
+
for await (const e of events) out.push(e)
|
|
22
|
+
return out
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function gatesIn(events: readonly BaseEvent[]): GateOpenedValue[] {
|
|
26
|
+
const out: GateOpenedValue[] = []
|
|
27
|
+
for (const e of events) {
|
|
28
|
+
const gate = readGateOpened(e)
|
|
29
|
+
if (gate) out.push(gate)
|
|
30
|
+
}
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assert(cond: unknown, message: string): asserts cond {
|
|
35
|
+
if (!cond) throw new Error(`conformance: ${message}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type WithToolCall = BaseEvent & { toolCallId?: string; toolCallName?: string }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A provider-agnostic conformance suite: any Provider implementation can be run
|
|
42
|
+
* against these checks to prove it honours the framework's gate/effect contract.
|
|
43
|
+
*/
|
|
44
|
+
export const providerConformanceChecks: ConformanceCheck[] = [
|
|
45
|
+
{
|
|
46
|
+
name: 'turn 1 opens exactly one approval gate matching an approval tool',
|
|
47
|
+
async run(makeProvider, scenario) {
|
|
48
|
+
const events = await collect(makeProvider().run(scenario.turn1Input))
|
|
49
|
+
const gates = gatesIn(events)
|
|
50
|
+
assert(gates.length === 1, `expected 1 GATE_OPENED, got ${gates.length}`)
|
|
51
|
+
const gate = gates[0]!
|
|
52
|
+
assert(
|
|
53
|
+
scenario.approvalNames.includes(gate.toolName),
|
|
54
|
+
`gate toolName "${gate.toolName}" is not an approval`,
|
|
55
|
+
)
|
|
56
|
+
const startIds = events
|
|
57
|
+
.filter((e) => e.type === EventType.TOOL_CALL_START)
|
|
58
|
+
.map((e) => (e as WithToolCall).toolCallId)
|
|
59
|
+
assert(startIds.includes(gate.toolCallId), 'gate toolCallId has no matching TOOL_CALL_START')
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'resume(approved) completes and re-opens no gate',
|
|
64
|
+
async run(makeProvider, scenario) {
|
|
65
|
+
const provider = makeProvider()
|
|
66
|
+
assert(typeof provider.resume === 'function', 'provider does not implement resume()')
|
|
67
|
+
const events = await collect(provider.resume!(scenario.approved.handle, scenario.approved.resolution))
|
|
68
|
+
assert(gatesIn(events).length === 0, 'resume(approved) re-opened a gate')
|
|
69
|
+
assert(events.length > 0, 'resume(approved) produced no events')
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'resume(rejected) terminates and re-opens no gate',
|
|
74
|
+
async run(makeProvider, scenario) {
|
|
75
|
+
const provider = makeProvider()
|
|
76
|
+
assert(typeof provider.resume === 'function', 'provider does not implement resume()')
|
|
77
|
+
const events = await collect(provider.resume!(scenario.rejected.handle, scenario.rejected.resolution))
|
|
78
|
+
assert(gatesIn(events).length === 0, 'resume(rejected) re-opened a gate')
|
|
79
|
+
assert(events.length > 0, 'resume(rejected) produced no events')
|
|
80
|
+
assert(
|
|
81
|
+
events.filter((e) => e.type === EventType.TOOL_CALL_START).length === 0,
|
|
82
|
+
'resume(rejected) emitted a tool call (possible effect)',
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'only surfaced tools appear as tool calls on turn 1',
|
|
88
|
+
async run(makeProvider, scenario) {
|
|
89
|
+
const events = await collect(makeProvider().run(scenario.turn1Input))
|
|
90
|
+
const toolNames = events
|
|
91
|
+
.filter((e) => e.type === EventType.TOOL_CALL_START)
|
|
92
|
+
.map((e) => (e as WithToolCall).toolCallName)
|
|
93
|
+
for (const name of toolNames) {
|
|
94
|
+
assert(
|
|
95
|
+
name !== undefined && scenario.surfaceTools.includes(name),
|
|
96
|
+
`surfaced an undeclared tool: "${name}"`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'every TOOL_CALL_START on turn 1 has a matching TOOL_CALL_END with the same toolCallId',
|
|
103
|
+
async run(makeProvider, scenario) {
|
|
104
|
+
const events = await collect(makeProvider().run(scenario.turn1Input))
|
|
105
|
+
const startIds = events
|
|
106
|
+
.filter((e) => e.type === EventType.TOOL_CALL_START)
|
|
107
|
+
.map((e) => (e as WithToolCall).toolCallId)
|
|
108
|
+
const endIds = new Set(
|
|
109
|
+
events
|
|
110
|
+
.filter((e) => e.type === EventType.TOOL_CALL_END)
|
|
111
|
+
.map((e) => (e as WithToolCall).toolCallId),
|
|
112
|
+
)
|
|
113
|
+
for (const id of startIds) {
|
|
114
|
+
assert(endIds.has(id), `TOOL_CALL_START id "${id}" has no matching TOOL_CALL_END`)
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The agent contract. `superRefine` enforces the framework's structural rules:
|
|
5
|
+
* approvals/renders/dispatches must be declared tools, and every effect must be
|
|
6
|
+
* an approval (so a side effect can never fire without a human gate).
|
|
7
|
+
*/
|
|
8
|
+
export const AgentDefinitionSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
name: z.string(),
|
|
12
|
+
provider: z.string(),
|
|
13
|
+
instructions: z.string(),
|
|
14
|
+
tools: z.array(z.string()),
|
|
15
|
+
approvals: z.array(z.string()),
|
|
16
|
+
renders: z.record(z.string()),
|
|
17
|
+
handoffs: z.array(z.string()).optional(),
|
|
18
|
+
maxInstances: z.number().int().positive().default(1),
|
|
19
|
+
effects: z.array(z.string()).default([]),
|
|
20
|
+
readonly: z.array(z.string()).default([]),
|
|
21
|
+
dispatches: z.array(z.string()).default([]),
|
|
22
|
+
})
|
|
23
|
+
.superRefine((def, ctx) => {
|
|
24
|
+
for (const approval of def.approvals) {
|
|
25
|
+
if (!def.tools.includes(approval)) {
|
|
26
|
+
ctx.addIssue({
|
|
27
|
+
code: z.ZodIssueCode.custom,
|
|
28
|
+
message: `approval "${approval}" is not declared in tools`,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const renderKey of Object.keys(def.renders)) {
|
|
33
|
+
if (!def.tools.includes(renderKey)) {
|
|
34
|
+
ctx.addIssue({
|
|
35
|
+
code: z.ZodIssueCode.custom,
|
|
36
|
+
message: `render key "${renderKey}" is not declared in tools`,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const effect of def.effects) {
|
|
41
|
+
if (!def.approvals.includes(effect)) {
|
|
42
|
+
ctx.addIssue({
|
|
43
|
+
code: z.ZodIssueCode.custom,
|
|
44
|
+
message: `effect "${effect}" is not an approval`,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const dispatch of def.dispatches) {
|
|
49
|
+
if (!def.tools.includes(dispatch)) {
|
|
50
|
+
ctx.addIssue({
|
|
51
|
+
code: z.ZodIssueCode.custom,
|
|
52
|
+
message: `dispatch "${dispatch}" is not declared in tools`,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export type AgentDefinition = z.infer<typeof AgentDefinitionSchema>
|
|
59
|
+
export type AgentDefinitionInput = z.input<typeof AgentDefinitionSchema>
|
|
60
|
+
|
|
61
|
+
export function defineAgent(def: AgentDefinitionInput): AgentDefinition {
|
|
62
|
+
return AgentDefinitionSchema.parse(def)
|
|
63
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RunAgentInput } from '@ag-ui/client'
|
|
2
|
+
import type { z } from 'zod'
|
|
3
|
+
import { decodeHandoff } from './handoff.js'
|
|
4
|
+
import type { PromptStrategy, ResumeOutcome } from './types.js'
|
|
5
|
+
|
|
6
|
+
export interface PromptSpec<T> {
|
|
7
|
+
input?: z.ZodType<T>
|
|
8
|
+
onInput?: (payload: T) => string
|
|
9
|
+
onStart: () => string
|
|
10
|
+
onResume?: (result: Record<string, unknown>) => ResumeOutcome
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a prompt strategy: on the first turn, decode any handoff payload and
|
|
15
|
+
* route it through `onInput`, else `onStart`; on resume, hand the executed
|
|
16
|
+
* result to `onResume`.
|
|
17
|
+
*/
|
|
18
|
+
export function definePrompt<T>(spec: PromptSpec<T>): PromptStrategy {
|
|
19
|
+
const { onResume } = spec
|
|
20
|
+
return {
|
|
21
|
+
buildFirst(input: RunAgentInput): string {
|
|
22
|
+
if (spec.input && spec.onInput) {
|
|
23
|
+
const payload = decodeHandoff(input, spec.input)
|
|
24
|
+
if (payload) return spec.onInput(payload)
|
|
25
|
+
}
|
|
26
|
+
return spec.onStart()
|
|
27
|
+
},
|
|
28
|
+
buildResume: onResume
|
|
29
|
+
? (_args: Record<string, unknown>, executedResult?: Record<string, unknown>) =>
|
|
30
|
+
onResume(executedResult ?? {})
|
|
31
|
+
: undefined,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { WorkflowDescriptor } from './types.js'
|
|
2
|
+
|
|
3
|
+
/** Stable instance id for an agent within a workflow. */
|
|
4
|
+
export function instanceId(workflowId: string, agentId: string): string {
|
|
5
|
+
return `${workflowId}__${agentId}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate a workflow descriptor's internal consistency: unique agent ids, a
|
|
10
|
+
* role:input entry agent, handoffs that point inside the workflow, unique input
|
|
11
|
+
* names, and inputs bound to input-role agents. Returns the descriptor as-is.
|
|
12
|
+
*/
|
|
13
|
+
export function defineWorkflow(def: WorkflowDescriptor): WorkflowDescriptor {
|
|
14
|
+
const agentIds = def.agents.map((a) => a.agent.id)
|
|
15
|
+
const dupAgent = agentIds.find((id, i) => agentIds.indexOf(id) !== i)
|
|
16
|
+
if (dupAgent) throw new Error(`workflow "${def.id}": duplicate agent id "${dupAgent}"`)
|
|
17
|
+
|
|
18
|
+
const inputAgentIds = new Set(
|
|
19
|
+
def.agents.filter((a) => a.role === 'input').map((a) => a.agent.id),
|
|
20
|
+
)
|
|
21
|
+
const allAgentIds = new Set(agentIds)
|
|
22
|
+
|
|
23
|
+
if (!inputAgentIds.has(def.entryAgentId)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`workflow "${def.id}": entry agent "${def.entryAgentId}" is not a role:input agent`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const a of def.agents) {
|
|
30
|
+
for (const target of a.agent.handoffs ?? []) {
|
|
31
|
+
if (!allAgentIds.has(target)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`workflow "${def.id}": agent "${a.agent.id}" hands off to "${target}" which is not in this workflow`,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const inputNames = def.inputs.map((i) => i.name)
|
|
40
|
+
const dupInput = inputNames.find((n, i) => inputNames.indexOf(n) !== i)
|
|
41
|
+
if (dupInput) throw new Error(`workflow "${def.id}": duplicate published input name "${dupInput}"`)
|
|
42
|
+
|
|
43
|
+
for (const input of def.inputs) {
|
|
44
|
+
if (!inputAgentIds.has(input.agentId)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`workflow "${def.id}": input "${input.name}" is bound to "${input.agentId}" which is not a role:input agent`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return def
|
|
52
|
+
}
|
package/src/delivery.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { instanceId } from './defineWorkflow.js'
|
|
2
|
+
import type { DeliveryResult, Destination, WorkflowDescriptor } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve where a payload should be delivered. Agent destinations resolve
|
|
6
|
+
* directly; contract destinations validate the payload against the target
|
|
7
|
+
* workflow's published input schema before resolving.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveDelivery(
|
|
10
|
+
workflows: WorkflowDescriptor[],
|
|
11
|
+
origin: string,
|
|
12
|
+
dest: Destination,
|
|
13
|
+
payload: unknown,
|
|
14
|
+
): DeliveryResult {
|
|
15
|
+
if (dest.kind === 'agent') {
|
|
16
|
+
return { ok: true, instanceId: instanceId(origin, dest.agentId) }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const workflow = workflows.find((w) => w.id === dest.workflow)
|
|
20
|
+
if (!workflow) return { ok: false, error: `unknown workflow "${dest.workflow}"` }
|
|
21
|
+
|
|
22
|
+
const input = workflow.inputs.find((i) => i.name === dest.input)
|
|
23
|
+
if (!input) return { ok: false, error: `workflow "${dest.workflow}" has no input "${dest.input}"` }
|
|
24
|
+
|
|
25
|
+
return input.schema.safeParse(payload).success
|
|
26
|
+
? { ok: true, instanceId: instanceId(workflow.id, input.agentId), targetWorkflow: workflow.id }
|
|
27
|
+
: { ok: false, error: `payload does not match contract "${dest.workflow}.${dest.input}"` }
|
|
28
|
+
}
|
package/src/fold.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { EventType } from '@ag-ui/client'
|
|
2
|
+
import type { BaseEvent, Message } from '@ag-ui/client'
|
|
3
|
+
import type { Outcome } from './types.js'
|
|
4
|
+
import { LIFECYCLE_NOTE_TEXT } from './lifecycleNote.js'
|
|
5
|
+
|
|
6
|
+
type FoldToolCall = {
|
|
7
|
+
id: string
|
|
8
|
+
type: 'function'
|
|
9
|
+
function: { name: string; arguments: string }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FoldMessage = {
|
|
13
|
+
id: string
|
|
14
|
+
role: string
|
|
15
|
+
content?: string
|
|
16
|
+
toolCalls?: FoldToolCall[]
|
|
17
|
+
toolCallId?: string
|
|
18
|
+
targetAgentId?: string
|
|
19
|
+
childWorkItemId?: string
|
|
20
|
+
deduped?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type StreamEvent = BaseEvent & {
|
|
24
|
+
messageId?: string
|
|
25
|
+
delta?: string
|
|
26
|
+
toolCallId?: string
|
|
27
|
+
parentMessageId?: string
|
|
28
|
+
toolCallName?: string
|
|
29
|
+
content?: string
|
|
30
|
+
name?: string
|
|
31
|
+
value?: {
|
|
32
|
+
outcome?: Outcome
|
|
33
|
+
at?: number
|
|
34
|
+
targetAgentId?: string
|
|
35
|
+
childWorkItemId?: string
|
|
36
|
+
deduped?: boolean
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fold an ag-ui event stream into a flat list of messages: text chunks merge by
|
|
42
|
+
* messageId, tool calls attach to their parent assistant message, tool results
|
|
43
|
+
* become tool messages, and lifecycle/handoff custom events become notes.
|
|
44
|
+
*/
|
|
45
|
+
export function foldEventsToMessages(events: readonly BaseEvent[]): Message[] {
|
|
46
|
+
const messages = new Map<string, FoldMessage>()
|
|
47
|
+
const parentByToolCall = new Map<string, string>()
|
|
48
|
+
|
|
49
|
+
for (const raw of events) {
|
|
50
|
+
const e = raw as StreamEvent
|
|
51
|
+
switch (e.type) {
|
|
52
|
+
case EventType.TEXT_MESSAGE_CHUNK: {
|
|
53
|
+
const id = e.messageId
|
|
54
|
+
if (!id) break
|
|
55
|
+
const existing = messages.get(id)
|
|
56
|
+
if (existing && existing.role === 'assistant') {
|
|
57
|
+
existing.content = (existing.content ?? '') + (e.delta ?? '')
|
|
58
|
+
} else {
|
|
59
|
+
messages.set(id, { id, role: 'assistant', content: e.delta ?? '' })
|
|
60
|
+
}
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
case EventType.TOOL_CALL_START: {
|
|
64
|
+
const id = e.toolCallId
|
|
65
|
+
if (!id) break
|
|
66
|
+
const parentId = e.parentMessageId ?? `tc-${id}`
|
|
67
|
+
let parent = messages.get(parentId)
|
|
68
|
+
if (!parent || parent.role !== 'assistant') {
|
|
69
|
+
parent = { id: parentId, role: 'assistant', content: '', toolCalls: [] }
|
|
70
|
+
messages.set(parentId, parent)
|
|
71
|
+
}
|
|
72
|
+
parent.toolCalls ||= []
|
|
73
|
+
parent.toolCalls.push({
|
|
74
|
+
id,
|
|
75
|
+
type: 'function',
|
|
76
|
+
function: { name: e.toolCallName ?? '', arguments: '' },
|
|
77
|
+
})
|
|
78
|
+
parentByToolCall.set(id, parentId)
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
case EventType.TOOL_CALL_ARGS: {
|
|
82
|
+
const id = e.toolCallId
|
|
83
|
+
if (!id) break
|
|
84
|
+
const parentId = parentByToolCall.get(id)
|
|
85
|
+
if (!parentId) break
|
|
86
|
+
const call = messages.get(parentId)?.toolCalls?.find((c) => c.id === id)
|
|
87
|
+
if (call) call.function.arguments += e.delta ?? ''
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
case EventType.TOOL_CALL_END:
|
|
91
|
+
break
|
|
92
|
+
case EventType.TOOL_CALL_RESULT: {
|
|
93
|
+
const id = e.toolCallId
|
|
94
|
+
const msgId = e.messageId ?? (id ? `result-${id}` : undefined)
|
|
95
|
+
if (!msgId || !id) break
|
|
96
|
+
messages.set(msgId, { id: msgId, role: 'tool', toolCallId: id, content: e.content ?? '' })
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
case EventType.CUSTOM: {
|
|
100
|
+
if (e.name === 'lifecycle') {
|
|
101
|
+
const v = e.value
|
|
102
|
+
if (v && v.outcome !== undefined) {
|
|
103
|
+
const text = LIFECYCLE_NOTE_TEXT[v.outcome] || v.outcome
|
|
104
|
+
const id = `lifecycle-${v.at}`
|
|
105
|
+
messages.set(id, { id, role: 'system', content: text })
|
|
106
|
+
}
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
if (e.name === 'handoff' && e.value) {
|
|
110
|
+
const v = e.value
|
|
111
|
+
const id = `handoff-${v.childWorkItemId}`
|
|
112
|
+
messages.set(id, {
|
|
113
|
+
id,
|
|
114
|
+
role: 'handoff',
|
|
115
|
+
targetAgentId: v.targetAgentId,
|
|
116
|
+
childWorkItemId: v.childWorkItemId,
|
|
117
|
+
deduped: v.deduped,
|
|
118
|
+
})
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return [...messages.values()] as unknown as Message[]
|
|
129
|
+
}
|
package/src/gate.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EventType } from '@ag-ui/client'
|
|
2
|
+
import type { BaseEvent, CustomEvent as AguiCustomEvent } from '@ag-ui/client'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
export const GATE_OPENED = 'GATE_OPENED' as const
|
|
6
|
+
|
|
7
|
+
export const GateOpenedValueSchema = z.object({
|
|
8
|
+
gateKind: z.literal('approval'),
|
|
9
|
+
toolName: z.string(),
|
|
10
|
+
toolCallId: z.string(),
|
|
11
|
+
proposedArtifact: z.record(z.unknown()),
|
|
12
|
+
})
|
|
13
|
+
export type GateOpenedValue = z.infer<typeof GateOpenedValueSchema>
|
|
14
|
+
|
|
15
|
+
/** Emit the custom event that opens a human-approval gate. */
|
|
16
|
+
export function gateOpened(value: GateOpenedValue): AguiCustomEvent {
|
|
17
|
+
return { type: EventType.CUSTOM, name: GATE_OPENED, value } as AguiCustomEvent
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Read a GATE_OPENED value off an event, or null if it isn't one. */
|
|
21
|
+
export function readGateOpened(event: BaseEvent): GateOpenedValue | null {
|
|
22
|
+
const e = event as AguiCustomEvent
|
|
23
|
+
if (e.type !== EventType.CUSTOM || e.name !== GATE_OPENED) return null
|
|
24
|
+
const parsed = GateOpenedValueSchema.safeParse(e.value)
|
|
25
|
+
return parsed.success ? parsed.data : null
|
|
26
|
+
}
|