@jigging/agent-method 0.0.0 → 0.1.0-alpha.3
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/AGENTS.md +98 -0
- package/FLOW.contract.json +241 -0
- package/FLOW.meta.json +8 -0
- package/FLOW.ts +3 -0
- package/LICENSE +373 -0
- package/README.md +369 -0
- package/THIRD_PARTY_NOTICES +9 -0
- package/contracts/acp-public-updates.json +75 -0
- package/contracts/agent-commands.json +88 -0
- package/contracts/agent-replies.json +202 -0
- package/contracts/http-request/contract.json +37 -0
- package/dist/api.d.ts +11 -0
- package/dist/api.js +164 -0
- package/dist/conversation.d.ts +68 -0
- package/dist/conversation.js +346 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +8 -0
- package/dist/flow.d.ts +3 -0
- package/dist/flow.js +2784 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +220 -0
- package/dist/json.d.ts +21 -0
- package/dist/json.js +409 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +180 -0
- package/dist/skills.d.ts +3 -0
- package/dist/skills.js +132 -0
- package/dist/values.d.ts +11 -0
- package/dist/values.js +65 -0
- package/justfile +32 -0
- package/licenses/flow.LICENSE +202 -0
- package/package.json +44 -4
- package/settings.schema.json +12 -0
- package/skills/answer-check/SKILL.md +5 -0
- package/src/api.ts +191 -0
- package/src/conversation.ts +387 -0
- package/src/errors.ts +11 -0
- package/src/flow.ts +66 -0
- package/src/index.ts +325 -0
- package/src/json.ts +406 -0
- package/src/schema.ts +230 -0
- package/src/skills.ts +138 -0
- package/src/values.ts +77 -0
- package/test/api.test.ts +326 -0
- package/test/conversation-fixture.ts +70 -0
- package/test/conversation.test.ts +305 -0
- package/test/json.test.ts +88 -0
- package/test/method.test.ts +308 -0
- package/test/pack.test.ts +81 -0
- package/test/result.test.ts +103 -0
- package/test/skills-flow.test.ts +252 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { exactKeys, ordinaryRecord, validSessionReceipt } from './values.js';
|
|
2
|
+
/** Retains received answers and every primary/cleanup failure; never manufactures success. */
|
|
3
|
+
export class AgentConversationError extends AggregateError {
|
|
4
|
+
turns;
|
|
5
|
+
settlement;
|
|
6
|
+
constructor(errors, turns, settlement) {
|
|
7
|
+
super(errors, 'Agent conversation did not complete cleanly', { cause: errors[0] });
|
|
8
|
+
this.turns = turns;
|
|
9
|
+
this.settlement = settlement;
|
|
10
|
+
this.name = 'AgentConversationError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function deferred() {
|
|
14
|
+
let resolve;
|
|
15
|
+
let reject;
|
|
16
|
+
const promise = new Promise((yes, no) => {
|
|
17
|
+
resolve = yes;
|
|
18
|
+
reject = no;
|
|
19
|
+
});
|
|
20
|
+
// A caller may inspect another part of a turn first. Rejections are still
|
|
21
|
+
// returned to every waiter; this is not a global failure acknowledgement ledger.
|
|
22
|
+
void promise.catch(() => undefined);
|
|
23
|
+
return { promise, resolve, reject };
|
|
24
|
+
}
|
|
25
|
+
function invalid(message) {
|
|
26
|
+
return new Error(message);
|
|
27
|
+
}
|
|
28
|
+
/** Ordinary Agent Run caller logic: no ACP implementation, host imports, or provider access. */
|
|
29
|
+
export async function withAgentConversation(run, options, use) {
|
|
30
|
+
if (options.events && options.onEvent)
|
|
31
|
+
throw new TypeError('Choose onEvent or an external events writer, not both');
|
|
32
|
+
const requestedSession = options.input.session !== undefined;
|
|
33
|
+
const turns = [];
|
|
34
|
+
const errors = [];
|
|
35
|
+
const record = (error) => {
|
|
36
|
+
if (!errors.includes(error))
|
|
37
|
+
errors.push(error);
|
|
38
|
+
};
|
|
39
|
+
const reads = new AbortController();
|
|
40
|
+
const pending = new Set();
|
|
41
|
+
const tracked = () => {
|
|
42
|
+
const value = deferred();
|
|
43
|
+
pending.add(value);
|
|
44
|
+
void value.promise.then(() => pending.delete(value), () => pending.delete(value));
|
|
45
|
+
return value;
|
|
46
|
+
};
|
|
47
|
+
const fail = (error) => {
|
|
48
|
+
record(error);
|
|
49
|
+
reads.abort(error);
|
|
50
|
+
for (const value of pending)
|
|
51
|
+
value.reject(error);
|
|
52
|
+
};
|
|
53
|
+
let commands;
|
|
54
|
+
let replies;
|
|
55
|
+
let events;
|
|
56
|
+
let observer;
|
|
57
|
+
const observerStop = new AbortController();
|
|
58
|
+
const observationErrors = [];
|
|
59
|
+
let offered = false;
|
|
60
|
+
let work;
|
|
61
|
+
let pump;
|
|
62
|
+
let settlement;
|
|
63
|
+
let writerClosed = false;
|
|
64
|
+
let value;
|
|
65
|
+
let active = true;
|
|
66
|
+
let current = { turn: 0, result: tracked() };
|
|
67
|
+
let promptControl;
|
|
68
|
+
let interruptControl;
|
|
69
|
+
let closeControl;
|
|
70
|
+
let controls = 0;
|
|
71
|
+
let finished = false;
|
|
72
|
+
const send = async (message) => {
|
|
73
|
+
if (++controls > 64)
|
|
74
|
+
throw invalid('Conversation control limit is 64 commands');
|
|
75
|
+
await commands.send.send(message, { signal: run.signal });
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
commands = await run.channel({
|
|
79
|
+
contract: { slot: options.slot, channel: 'commands' },
|
|
80
|
+
});
|
|
81
|
+
replies = await run.channel({
|
|
82
|
+
contract: { slot: options.slot, channel: 'replies' },
|
|
83
|
+
});
|
|
84
|
+
if (options.onEvent) {
|
|
85
|
+
events = await run.channel({
|
|
86
|
+
contract: { slot: options.slot, channel: 'events' },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
offered = true;
|
|
90
|
+
// Do not cancel this local waiter to stop a conversation. Its real terminal
|
|
91
|
+
// is the evidence that lets a healthy parent subsequently use the capacity.
|
|
92
|
+
work = run
|
|
93
|
+
.call({
|
|
94
|
+
operationId: options.operationId,
|
|
95
|
+
slot: options.slot,
|
|
96
|
+
input: { ...options.input, conversation: true },
|
|
97
|
+
channels: {
|
|
98
|
+
commands: commands.receive,
|
|
99
|
+
replies: replies.send,
|
|
100
|
+
...(events ? { events: events.send } : options.events ? { events: options.events } : {}),
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
.then((result) => ({ result }), (error) => {
|
|
104
|
+
fail(error);
|
|
105
|
+
observerStop.abort(error);
|
|
106
|
+
return { error };
|
|
107
|
+
});
|
|
108
|
+
if (events) {
|
|
109
|
+
observer = (async () => {
|
|
110
|
+
try {
|
|
111
|
+
for (;;) {
|
|
112
|
+
const item = await events.receive.next({
|
|
113
|
+
signal: AbortSignal.any([run.signal, observerStop.signal]),
|
|
114
|
+
});
|
|
115
|
+
if (item.done)
|
|
116
|
+
break;
|
|
117
|
+
const returned = options.onEvent(item.value);
|
|
118
|
+
if (returned && typeof returned.then === 'function') {
|
|
119
|
+
void Promise.resolve(returned).catch(() => undefined);
|
|
120
|
+
throw new TypeError('onEvent must be synchronous; use events for asynchronous channel processing');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
observationErrors.push(error);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
try {
|
|
129
|
+
await events.receive.close();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (!observationErrors.includes(error))
|
|
133
|
+
observationErrors.push(error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
}
|
|
138
|
+
pump = (async () => {
|
|
139
|
+
try {
|
|
140
|
+
for (;;) {
|
|
141
|
+
const item = await replies.receive.next({
|
|
142
|
+
signal: AbortSignal.any([run.signal, reads.signal]),
|
|
143
|
+
});
|
|
144
|
+
if (item.done)
|
|
145
|
+
throw invalid('Agent replies ended before accepted conversation close');
|
|
146
|
+
const message = item.value;
|
|
147
|
+
if (!message ||
|
|
148
|
+
Array.isArray(message) ||
|
|
149
|
+
typeof message !== 'object' ||
|
|
150
|
+
message.turn !== current.turn)
|
|
151
|
+
throw invalid('Agent reply has an unexpected turn');
|
|
152
|
+
if (message.type === 'accepted' || message.type === 'rejected') {
|
|
153
|
+
if (message.command === 'prompt' && promptControl) {
|
|
154
|
+
const control = promptControl;
|
|
155
|
+
promptControl = undefined;
|
|
156
|
+
if (message.type === 'accepted')
|
|
157
|
+
control.accepted.resolve();
|
|
158
|
+
else {
|
|
159
|
+
const error = invalid(`Agent rejected prompt: ${String(message.code)}`);
|
|
160
|
+
current.result.reject(error);
|
|
161
|
+
current = control.previous;
|
|
162
|
+
active = false;
|
|
163
|
+
control.accepted.reject(error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (message.command === 'interrupt' && interruptControl) {
|
|
167
|
+
const control = interruptControl;
|
|
168
|
+
interruptControl = undefined;
|
|
169
|
+
if (message.type === 'accepted')
|
|
170
|
+
control.resolve('accepted');
|
|
171
|
+
else if (message.code === 'NOT_RUNNING')
|
|
172
|
+
control.resolve('not-running');
|
|
173
|
+
else
|
|
174
|
+
control.reject(invalid(`Agent rejected interruption: ${String(message.code)}`));
|
|
175
|
+
}
|
|
176
|
+
else if (message.command === 'close' && closeControl) {
|
|
177
|
+
if (message.type !== 'accepted')
|
|
178
|
+
throw invalid('Agent rejected conversation close');
|
|
179
|
+
closeControl.resolve();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
else
|
|
183
|
+
throw invalid('Agent sent an unexpected control reply');
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
if (!active || promptControl)
|
|
187
|
+
throw invalid('Agent sent an out-of-order turn result');
|
|
188
|
+
if (message.type === 'result') {
|
|
189
|
+
const result = message.result;
|
|
190
|
+
if (!result || typeof result.outcome !== 'string' || !Object.hasOwn(result, 'output'))
|
|
191
|
+
throw invalid('Agent omitted its complete turn result');
|
|
192
|
+
}
|
|
193
|
+
else if (message.type !== 'cancelled' &&
|
|
194
|
+
!(message.type === 'error' &&
|
|
195
|
+
message.code === 'INVALID_RESULT' &&
|
|
196
|
+
typeof message.message === 'string'))
|
|
197
|
+
throw invalid('Agent sent an invalid turn terminal');
|
|
198
|
+
const turn = item.value;
|
|
199
|
+
if (turns.length >= 8)
|
|
200
|
+
throw invalid('Agent exceeded the eight-turn conversation limit');
|
|
201
|
+
turns.push(turn);
|
|
202
|
+
active = false;
|
|
203
|
+
current.result.resolve(turn);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (!reads.signal.aborted)
|
|
209
|
+
fail(error);
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
const conversation = {
|
|
213
|
+
initial: current.result.promise,
|
|
214
|
+
prompt(input) {
|
|
215
|
+
if (finished || active || interruptControl || errors.length)
|
|
216
|
+
return Promise.reject(invalid('Conversation is not ready for another prompt'));
|
|
217
|
+
if (current.turn >= 7)
|
|
218
|
+
return Promise.reject(invalid('Conversation turn limit reached'));
|
|
219
|
+
const previous = current;
|
|
220
|
+
current = { turn: previous.turn + 1, result: tracked() };
|
|
221
|
+
const result = current.result.promise;
|
|
222
|
+
active = true;
|
|
223
|
+
promptControl = { accepted: tracked(), previous };
|
|
224
|
+
void send({
|
|
225
|
+
type: 'prompt',
|
|
226
|
+
turn: current.turn,
|
|
227
|
+
input: input,
|
|
228
|
+
}).catch(fail);
|
|
229
|
+
return result;
|
|
230
|
+
},
|
|
231
|
+
async interrupt() {
|
|
232
|
+
if (finished || errors.length)
|
|
233
|
+
throw invalid('Conversation is closing or failed');
|
|
234
|
+
if (promptControl)
|
|
235
|
+
await promptControl.accepted.promise;
|
|
236
|
+
if (!active)
|
|
237
|
+
return 'not-running';
|
|
238
|
+
if (interruptControl)
|
|
239
|
+
return interruptControl.promise;
|
|
240
|
+
const control = tracked();
|
|
241
|
+
interruptControl = control;
|
|
242
|
+
void send({ type: 'interrupt', turn: current.turn }).catch(fail);
|
|
243
|
+
return control.promise;
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
try {
|
|
247
|
+
value = await use(conversation);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
record(error);
|
|
251
|
+
}
|
|
252
|
+
finished = true;
|
|
253
|
+
if (active)
|
|
254
|
+
record(invalid('Application left a live Agent turn unfinished'));
|
|
255
|
+
if (!active && !reads.signal.aborted && !run.signal.aborted) {
|
|
256
|
+
if (interruptControl)
|
|
257
|
+
await interruptControl.promise;
|
|
258
|
+
closeControl = tracked();
|
|
259
|
+
await send({ type: 'close', turn: current.turn });
|
|
260
|
+
await commands.send.close({ signal: run.signal });
|
|
261
|
+
writerClosed = true;
|
|
262
|
+
await closeControl.promise;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
record(error);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
finished = true;
|
|
270
|
+
// A failed callback/transport stops delivery to the Agent through ordinary
|
|
271
|
+
// channel closure, while preserving the non-cancelled invocation waiter.
|
|
272
|
+
if (commands && !writerClosed) {
|
|
273
|
+
try {
|
|
274
|
+
await commands.send.close({ error: 'LAGGED', signal: run.signal });
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
record(error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
reads.abort();
|
|
281
|
+
await pump;
|
|
282
|
+
if (work) {
|
|
283
|
+
const terminal = await work;
|
|
284
|
+
if ('error' in terminal)
|
|
285
|
+
record(terminal.error);
|
|
286
|
+
else
|
|
287
|
+
settlement = terminal.result;
|
|
288
|
+
}
|
|
289
|
+
await observer;
|
|
290
|
+
if (events && !offered) {
|
|
291
|
+
try {
|
|
292
|
+
await events.send.close({ error: 'LAGGED', signal: run.signal });
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
record(error);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
await events.receive.close();
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
record(error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (replies) {
|
|
305
|
+
try {
|
|
306
|
+
await replies.receive.close();
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
record(error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// An allocated receiver that was never offered is still ours.
|
|
313
|
+
if (commands && !offered) {
|
|
314
|
+
try {
|
|
315
|
+
await commands.receive.close();
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
record(error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (run.signal.aborted)
|
|
323
|
+
record(run.signal.reason ?? invalid('Run cancelled'));
|
|
324
|
+
const output = ordinaryRecord(settlement?.output);
|
|
325
|
+
if (!settlement ||
|
|
326
|
+
settlement.outcome !== 'done' ||
|
|
327
|
+
!output ||
|
|
328
|
+
output.turns !== turns.length ||
|
|
329
|
+
!exactKeys(output, ['turns', ...(requestedSession ? ['session'] : [])]) ||
|
|
330
|
+
(requestedSession && !validSessionReceipt(output.session)))
|
|
331
|
+
record(invalid('Agent omitted matching conversation settlement'));
|
|
332
|
+
if (errors.length)
|
|
333
|
+
throw new AgentConversationError(errors, Object.freeze(turns), settlement);
|
|
334
|
+
return {
|
|
335
|
+
value,
|
|
336
|
+
turns: Object.freeze(turns),
|
|
337
|
+
settlement: settlement,
|
|
338
|
+
...(options.onEvent
|
|
339
|
+
? {
|
|
340
|
+
observation: observationErrors.length
|
|
341
|
+
? { status: 'incomplete', errors: Object.freeze(observationErrors) }
|
|
342
|
+
: { status: 'complete' },
|
|
343
|
+
}
|
|
344
|
+
: {}),
|
|
345
|
+
};
|
|
346
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
package/dist/flow.d.ts
ADDED