@dxos/agent-runtime 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/chunk-AgentService.mjs +716 -362
- package/dist/lib/chunk-AgentService.mjs.map +1 -1
- package/dist/lib/index.mjs +2 -2
- package/dist/lib/testing.mjs +41 -29
- package/dist/lib/testing.mjs.map +1 -1
- package/dist/types/src/agent-service/AgentService.d.ts +27 -12
- package/dist/types/src/agent-service/AgentService.d.ts.map +1 -1
- package/dist/types/src/agent-service/agent-process.d.ts +580 -206
- package/dist/types/src/agent-service/agent-process.d.ts.map +1 -1
- package/dist/types/src/agent-service/delegation-strategy.d.ts +17 -8
- package/dist/types/src/agent-service/delegation-strategy.d.ts.map +1 -1
- package/dist/types/src/agent-service/index.d.ts +4 -4
- package/dist/types/src/agent-service/index.d.ts.map +1 -1
- package/dist/types/src/agent-service/mcp-servers.d.ts +10 -0
- package/dist/types/src/agent-service/mcp-servers.d.ts.map +1 -0
- package/dist/types/src/agent-service/mcp-servers.test.d.ts +2 -0
- package/dist/types/src/agent-service/mcp-servers.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/process-selection.test.d.ts +2 -0
- package/dist/types/src/agent-service/process-selection.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/queue-scripted.test.d.ts +2 -0
- package/dist/types/src/agent-service/queue-scripted.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/skill-hooks.test.d.ts +2 -0
- package/dist/types/src/agent-service/skill-hooks.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/tool-backgrounding.test.d.ts +2 -0
- package/dist/types/src/agent-service/tool-backgrounding.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/turn-producer.d.ts +5 -1
- package/dist/types/src/agent-service/turn-producer.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/assistant-test-layer.d.ts +12 -10
- package/dist/types/src/testing/assistant-test-layer.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +4 -4
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent.d.ts +4 -5
- package/dist/types/src/testing/test-agent.d.ts.map +1 -1
- package/dist/types/src/testing/trace-pretty-print.d.ts +1 -1
- package/dist/types/src/testing/trace-pretty-print.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -18
- package/src/agent-service/AgentService.test.ts +351 -64
- package/src/agent-service/AgentService.ts +304 -149
- package/src/agent-service/README.md +16 -14
- package/src/agent-service/agent-process.test.ts +63 -131
- package/src/agent-service/agent-process.ts +714 -356
- package/src/agent-service/delegation-scripted.test.ts +9 -9
- package/src/agent-service/delegation-strategy.ts +25 -8
- package/src/agent-service/index.ts +13 -4
- package/src/agent-service/mcp-servers.test.ts +99 -0
- package/src/agent-service/mcp-servers.ts +62 -0
- package/src/agent-service/process-selection.test.ts +135 -0
- package/src/agent-service/queue-scripted.test.ts +461 -0
- package/src/agent-service/redelivery.test.ts +52 -28
- package/src/agent-service/skill-hooks.test.ts +182 -0
- package/src/agent-service/tool-backgrounding.test.ts +249 -0
- package/src/agent-service/turn-producer.ts +6 -2
- package/src/assistant-session-tests/format.test.ts +1 -1
- package/src/assistant-session-tests/request.test.ts +3 -3
- package/src/assistant-session-tests/scripted-loop.test.ts +99 -3
- package/src/assistant-session-tests/session-ollama.test.ts +1 -1
- package/src/assistant-session-tests/xml-response.test.ts +1 -1
- package/src/functions.test.ts +1 -1
- package/src/index.ts +1 -1
- package/src/testing/assistant-test-layer.ts +84 -35
- package/src/testing/index.ts +4 -4
- package/src/testing/test-agent.ts +8 -9
- package/src/testing/trace-pretty-print.ts +3 -4
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
import * as Cause from "effect/Cause";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import * as Layer from "effect/Layer";
|
|
4
|
-
import * as
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
4
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
5
|
+
import * as Semaphore from "effect/Semaphore";
|
|
6
|
+
import { AiContext, AiSession, Alarm, HarnessControl, SessionStore, SkillHooks, getOperationFromTool, makeToolExecutionService, makeToolResolverFromOperations } from "@dxos/assistant";
|
|
7
|
+
import * as Chat from "@dxos/assistant/Chat";
|
|
8
|
+
import * as AgentService from "@dxos/compute/AgentService";
|
|
8
9
|
import * as Process from "@dxos/compute/Process";
|
|
9
10
|
import * as Skill from "@dxos/compute/Skill";
|
|
10
|
-
import { Annotation, Database, Feed, Obj, Ref, Registry } from "@dxos/echo";
|
|
11
|
-
import
|
|
11
|
+
import { Annotation, Database, Feed, Filter, Obj, Ref, Registry } from "@dxos/echo";
|
|
12
|
+
import * as EffectEx from "@dxos/effect/EffectEx";
|
|
12
13
|
import { DXN, EID } from "@dxos/keys";
|
|
13
14
|
import { log } from "@dxos/log";
|
|
15
|
+
import * as Tool from "effect/ai/Tool";
|
|
16
|
+
import * as Toolkit from "effect/ai/Toolkit";
|
|
14
17
|
import * as Clock from "effect/Clock";
|
|
15
18
|
import * as DateTime from "effect/DateTime";
|
|
16
19
|
import * as Duration from "effect/Duration";
|
|
17
20
|
import * as Exit from "effect/Exit";
|
|
18
|
-
import * as
|
|
21
|
+
import * as Option from "effect/Option";
|
|
19
22
|
import * as Schema from "effect/Schema";
|
|
20
23
|
import * as Struct from "effect/Struct";
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import { AiService, OpaqueToolkit } from "@dxos/ai";
|
|
24
|
+
import { AiService, Model, OpaqueToolkit } from "@dxos/ai";
|
|
25
|
+
import { OperationProcess } from "@dxos/compute-runtime";
|
|
24
26
|
import * as Credential from "@dxos/compute/Credential";
|
|
25
|
-
import * as
|
|
27
|
+
import * as McpServer from "@dxos/compute/McpServer";
|
|
26
28
|
import * as Operation from "@dxos/compute/Operation";
|
|
29
|
+
import * as OperationHandlerSet from "@dxos/compute/OperationHandlerSet";
|
|
27
30
|
import * as StorageService from "@dxos/compute/StorageService";
|
|
28
31
|
import * as Trace from "@dxos/compute/Trace";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
32
|
+
import { AccessToken } from "@dxos/link";
|
|
33
|
+
import { Actor, ContentBlock, Message } from "@dxos/types";
|
|
34
|
+
import { markWork, trim } from "@dxos/util";
|
|
35
|
+
import { McpOAuth } from "@dxos/mcp-client";
|
|
31
36
|
//#region \0rolldown/runtime.js
|
|
32
37
|
var __defProp = Object.defineProperty;
|
|
33
38
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -40,6 +45,51 @@ var __exportAll = (all, no_symbols) => {
|
|
|
40
45
|
return target;
|
|
41
46
|
};
|
|
42
47
|
//#endregion
|
|
48
|
+
//#region src/agent-service/mcp-servers.ts
|
|
49
|
+
var __dxlog_file$2 = "/__w/dxos/dxos/packages/core/compute/agent-runtime/src/agent-service/mcp-servers.ts";
|
|
50
|
+
/**
|
|
51
|
+
* The space's enabled MCP servers as connection options, read at the start of every turn so a server
|
|
52
|
+
* added, toggled or signed in to from the chat's options applies from the next turn on.
|
|
53
|
+
*/
|
|
54
|
+
var loadSpaceMcpServers = () => Effect.gen(function* () {
|
|
55
|
+
const servers = yield* Database.query(Filter.type(McpServer.McpServer)).run;
|
|
56
|
+
return yield* Effect.forEach(servers.filter((server) => server.enabled !== false), toOptions);
|
|
57
|
+
});
|
|
58
|
+
var toOptions = (server) => Effect.gen(function* () {
|
|
59
|
+
const options = {
|
|
60
|
+
url: server.url,
|
|
61
|
+
protocol: server.protocol,
|
|
62
|
+
apiKey: server.apiKey
|
|
63
|
+
};
|
|
64
|
+
if (server.oauth?.tokens) return {
|
|
65
|
+
...options,
|
|
66
|
+
authProvider: McpOAuth.makeProvider({ store: McpServer.oauthStore(server) })
|
|
67
|
+
};
|
|
68
|
+
if (server.accessToken) {
|
|
69
|
+
const apiKey = yield* resolveAccessToken(server.accessToken);
|
|
70
|
+
return Option.isSome(apiKey) ? {
|
|
71
|
+
...options,
|
|
72
|
+
apiKey: apiKey.value
|
|
73
|
+
} : options;
|
|
74
|
+
}
|
|
75
|
+
return options;
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* Resolves through {@link Credential.CredentialsService} rather than reading `token` directly, so a
|
|
79
|
+
* token EDGE custodies (a managed one) resolves the same way a pasted one does.
|
|
80
|
+
*/
|
|
81
|
+
var resolveAccessToken = (ref) => Effect.gen(function* () {
|
|
82
|
+
const accessToken = yield* Database.load(ref);
|
|
83
|
+
const credentials = yield* Credential.CredentialsService;
|
|
84
|
+
const credential = yield* Effect.tryPromise(() => credentials.getCredential({ accessTokenId: accessToken.id }));
|
|
85
|
+
return Option.fromNullishOr(credential.apiKey);
|
|
86
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => log.warn("failed to resolve MCP server credential", { error }, {
|
|
87
|
+
"~LogMeta": "~LogMeta",
|
|
88
|
+
F: __dxlog_file$2,
|
|
89
|
+
L: 60,
|
|
90
|
+
S: void 0
|
|
91
|
+
}))), Effect.orElseSucceed(() => Option.none()));
|
|
92
|
+
//#endregion
|
|
43
93
|
//#region src/agent-service/turn-producer.ts
|
|
44
94
|
/**
|
|
45
95
|
* The default producer: `AiSession.Session`, which resolves the model, binds skills and context
|
|
@@ -57,224 +107,477 @@ var makeAiSessionTurnProducer = ({ feed, runtime, instructions }) => EffectEx.ac
|
|
|
57
107
|
//#region src/agent-service/agent-process.ts
|
|
58
108
|
var __dxlog_file$1 = "/__w/dxos/dxos/packages/core/compute/agent-runtime/src/agent-service/agent-process.ts";
|
|
59
109
|
var AGENT_PROCESS_KEY = "org.dxos.testing.process.agent";
|
|
110
|
+
var AgentPrompt = Schema.Union([Schema.String, Schema.Array(ContentBlock.Any)]);
|
|
111
|
+
/**
|
|
112
|
+
* Who sent a prompt: the plain-data subset of `Actor`, since the input crosses a JSON boundary
|
|
113
|
+
* (EDGE decodes it with the schema's type side) where a `Ref` cannot be supplied.
|
|
114
|
+
*/
|
|
115
|
+
var AgentInputSender = Actor.Actor.mapFields(Struct.pick([
|
|
116
|
+
"role",
|
|
117
|
+
"name",
|
|
118
|
+
"identityDid",
|
|
119
|
+
"email"
|
|
120
|
+
]));
|
|
121
|
+
/**
|
|
122
|
+
* Input accepted by {@link AgentProcess}: a bare prompt, or a prompt attributed to a sender (e.g. a
|
|
123
|
+
* relayed Discord author) whose identity the appended `Message` records.
|
|
124
|
+
*/
|
|
125
|
+
var AgentInput = Schema.Union([AgentPrompt, Schema.Struct({
|
|
126
|
+
prompt: AgentPrompt,
|
|
127
|
+
sender: Schema.optional(AgentInputSender),
|
|
128
|
+
/** Foreign identity and other source metadata copied onto the message (e.g. `{ discord: { userId } }`). */
|
|
129
|
+
properties: Schema.optional(Schema.Record(Schema.String, Schema.Any))
|
|
130
|
+
})]);
|
|
131
|
+
/** Builds the feed message for an input; the sender defaults to the plain user role. */
|
|
132
|
+
var makeInputMessage = (input) => {
|
|
133
|
+
const { prompt, sender, properties } = typeof input === "object" && "prompt" in input ? input : {
|
|
134
|
+
prompt: input,
|
|
135
|
+
sender: void 0,
|
|
136
|
+
properties: void 0
|
|
137
|
+
};
|
|
138
|
+
const blocks = typeof prompt === "string" ? [ContentBlock.Text.make({ text: prompt })] : [...prompt];
|
|
139
|
+
return Message.make({
|
|
140
|
+
sender: {
|
|
141
|
+
role: "user",
|
|
142
|
+
...sender
|
|
143
|
+
},
|
|
144
|
+
blocks,
|
|
145
|
+
properties
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* How long to wait before re-reading a queue that contradicts a write this process just made, and
|
|
150
|
+
* how many times. A hosted runtime serves the read from an eventually-consistent index, and the lag
|
|
151
|
+
* measured on EDGE is single-digit milliseconds — the cap is what keeps a write that never lands
|
|
152
|
+
* from waking the agent forever.
|
|
153
|
+
*/
|
|
154
|
+
var UNSEEN_WRITE_RETRY_MS = 250;
|
|
155
|
+
/** How much of a sub-agent's result the trace keeps: enough to read, never the whole payload. */
|
|
156
|
+
var RESULT_PREVIEW_LENGTH = 200;
|
|
157
|
+
/**
|
|
158
|
+
* A short, safe rendering of a sub-agent's result for the trace.
|
|
159
|
+
*
|
|
160
|
+
* Guarded because this sits between removing the child from `delegations` and running the strategy's
|
|
161
|
+
* `onComplete`: a `BigInt` or a cycle would throw here, become a defect, and take the whole return
|
|
162
|
+
* path with it — the work item would never be updated and work waiting on it never reconciled.
|
|
163
|
+
* Losing the preview is survivable; losing the return is not.
|
|
164
|
+
*/
|
|
165
|
+
var resultPreview = (value) => {
|
|
166
|
+
let text;
|
|
167
|
+
try {
|
|
168
|
+
text = typeof value === "string" ? value : JSON.stringify(value);
|
|
169
|
+
} catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (text === void 0) return;
|
|
173
|
+
return text.length > RESULT_PREVIEW_LENGTH ? `${text.slice(0, RESULT_PREVIEW_LENGTH)}…` : text;
|
|
174
|
+
};
|
|
175
|
+
var MAX_UNSEEN_WRITE_WAKES = 20;
|
|
60
176
|
/**
|
|
61
177
|
* Hosts a persistent, suspendible AiAgent that can process a number of prompts.
|
|
62
178
|
* The process target is a queue DXN string.
|
|
63
179
|
*/
|
|
64
|
-
var AgentProcess = (options) =>
|
|
180
|
+
var AgentProcess = (options) => Operation.makeDurable({
|
|
65
181
|
key: AGENT_PROCESS_KEY,
|
|
66
|
-
input:
|
|
182
|
+
input: AgentInput,
|
|
67
183
|
output: Schema.Void,
|
|
184
|
+
types: [
|
|
185
|
+
Chat.Chat,
|
|
186
|
+
Feed.Feed,
|
|
187
|
+
Message.Message,
|
|
188
|
+
Alarm.Alarm,
|
|
189
|
+
AiContext.Binding,
|
|
190
|
+
Skill.Skill,
|
|
191
|
+
McpServer.McpServer,
|
|
192
|
+
AccessToken.AccessToken
|
|
193
|
+
],
|
|
68
194
|
services: [
|
|
69
195
|
Database.Service,
|
|
70
196
|
OpaqueToolkit.OpaqueToolkitProvider,
|
|
71
197
|
Operation.Service,
|
|
72
198
|
Registry.Service,
|
|
73
199
|
StorageService.StorageService,
|
|
74
|
-
|
|
200
|
+
Process.ManagerService,
|
|
201
|
+
OperationHandlerSet.OperationHandlerProvider,
|
|
75
202
|
AiService.AiService,
|
|
76
203
|
Credential.CredentialsService
|
|
77
204
|
],
|
|
78
205
|
rpcs: HarnessControl
|
|
79
|
-
}
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const instructions =
|
|
206
|
+
}).pipe(Operation.withDurableHandler((ctx) => Effect.gen(function* () {
|
|
207
|
+
const chatDxn = Annotation.getDictionary(ctx.params.annotations, Process.TargetAnnotation).pipe(Option.getOrUndefined);
|
|
208
|
+
if (chatDxn == null) return yield* Effect.die(/* @__PURE__ */ new Error("Agent executable requires spawn options.target set to a Chat DXN."));
|
|
209
|
+
const chat = yield* Database.resolve(chatDxn, Chat.Chat).pipe(Effect.orDie);
|
|
210
|
+
const feed = yield* Database.load(chat.feed).pipe(Effect.orDie);
|
|
211
|
+
const instructions = chat.instructions ? yield* Database.load(chat.instructions).pipe(Effect.orElseSucceed(() => void 0)) : void 0;
|
|
85
212
|
const runtime = yield* Effect.context();
|
|
86
213
|
const session = yield* (options.makeTurnProducer ?? makeAiSessionTurnProducer)({
|
|
214
|
+
chat,
|
|
87
215
|
feed,
|
|
88
216
|
runtime,
|
|
89
217
|
instructions: instructions ? [instructions] : []
|
|
90
218
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
219
|
+
const sessionStore = new SessionStore();
|
|
220
|
+
let toolResults = [...yield* ToolResultsCell.get];
|
|
221
|
+
let ackedEntries = [...yield* AckedEntriesCell.get];
|
|
222
|
+
let selfWakes = yield* SelfWakesCell.get;
|
|
223
|
+
const toolCallManager = new ToolCallManager(yield* StorageService.StorageService);
|
|
94
224
|
yield* toolCallManager.load();
|
|
95
225
|
const clock = yield* Clock.Clock;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
226
|
+
const now = () => clock.currentTimeMillisUnsafe();
|
|
227
|
+
yield* toolCallManager.reconcileWithInputQueue(toolResults);
|
|
228
|
+
const unseenAlarms = /* @__PURE__ */ new Map();
|
|
229
|
+
const reconcileAlarmWith = (state) => {
|
|
230
|
+
for (const alarm of state.pendingAlarms) unseenAlarms.delete(alarm.id);
|
|
231
|
+
const observed = state.pendingAlarms[0]?.wakeAt;
|
|
232
|
+
const wakeAts = [...observed != null ? [observed] : [], ...[...unseenAlarms.values()].map(({ wakeAt }) => wakeAt)];
|
|
233
|
+
const delay = computeAlarmDelay({
|
|
234
|
+
hasPendingWork: toolResults.length > 0 || state.pendingMessages.length > 0,
|
|
235
|
+
wakeAt: wakeAts.length > 0 ? Math.min(...wakeAts) : null,
|
|
236
|
+
now: now()
|
|
237
|
+
});
|
|
238
|
+
return delay != null ? ctx.setAlarm(delay) : Effect.void;
|
|
239
|
+
};
|
|
240
|
+
const reconcileAlarm = Effect.flatMap(sessionStore.loadPending(feed), reconcileAlarmWith);
|
|
241
|
+
const startupPending = yield* sessionStore.loadPending(feed);
|
|
242
|
+
yield* reconcileAlarmWith(startupPending);
|
|
103
243
|
const strategy = Option.fromNullishOr(options.delegationStrategy);
|
|
104
244
|
let delegations = [...yield* DelegationsCell.get];
|
|
105
|
-
|
|
106
|
-
const
|
|
245
|
+
let asyncHooks = [...yield* AsyncHooksCell.get];
|
|
246
|
+
const untrackedExits = /* @__PURE__ */ new Set();
|
|
247
|
+
const model = chat.session?.model ?? options.defaultModel;
|
|
248
|
+
const requestModelLayer = AiService.languageModel(DXN.getName(model ?? Model.claudeSonnet5.id), { provider: options.provider });
|
|
249
|
+
const childServices = yield* Effect.context();
|
|
107
250
|
const runEndRequestHooks = Effect.gen(function* () {
|
|
108
251
|
yield* SkillHooks.runHooks({
|
|
109
252
|
skills: session.getSkills(),
|
|
110
253
|
phase: "end-request",
|
|
111
|
-
invoke: (operation, input) => Effect.gen(function* () {
|
|
112
|
-
const
|
|
254
|
+
invoke: (operation, input, hook) => Effect.gen(function* () {
|
|
255
|
+
const handle = yield* Process.spawn(OperationProcess.make(operation), input, {
|
|
113
256
|
environment: { conversation: Obj.getURI(feed) },
|
|
114
257
|
traceMeta: { conversation: Ref.make(feed) }
|
|
115
|
-
})
|
|
116
|
-
if (
|
|
117
|
-
|
|
258
|
+
});
|
|
259
|
+
if (hook.async) {
|
|
260
|
+
if (untrackedExits.delete(handle.pid)) return;
|
|
261
|
+
asyncHooks.push(handle.pid);
|
|
262
|
+
yield* AsyncHooksCell.set(asyncHooks);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
yield* Process.awaitOutput(handle);
|
|
266
|
+
}).pipe(Effect.provide(childServices), Effect.asVoid, Effect.orDie)
|
|
118
267
|
});
|
|
119
268
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
269
|
+
let turnRan = false;
|
|
270
|
+
let endHooksFired = asyncHooks.length > 0;
|
|
271
|
+
const unseenWriteIds = /* @__PURE__ */ new Set();
|
|
272
|
+
let unseenWriteWakes = 0;
|
|
273
|
+
const pendingWork = (state) => isAgentWorkPending({
|
|
274
|
+
toolResults,
|
|
275
|
+
pendingMessages: state.pendingMessages,
|
|
276
|
+
pendingAlarms: state.pendingAlarms,
|
|
277
|
+
delegations,
|
|
278
|
+
toolCallManager
|
|
279
|
+
});
|
|
280
|
+
const reportSleeping = (state) => state.pendingAlarms.length > 0 ? Trace.emitRequestPhase("sleeping") : Effect.void;
|
|
281
|
+
const maybeCompleteWith = (state) => Effect.gen(function* () {
|
|
282
|
+
for (const pid of dropReportedToolResults(toolResults, (pid) => toolCallManager.isReported(pid))) log("drop tool result reported within its turn", { pid }, {
|
|
283
|
+
"~LogMeta": "~LogMeta",
|
|
284
|
+
F: __dxlog_file$1,
|
|
285
|
+
L: 400,
|
|
286
|
+
S: this
|
|
287
|
+
});
|
|
288
|
+
if (pendingWork(state)) {
|
|
289
|
+
yield* reportSleeping(state);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!turnRan && !endHooksFired) {
|
|
293
|
+
log("agent idle before its first turn, staying resident", void 0, {
|
|
294
|
+
"~LogMeta": "~LogMeta",
|
|
295
|
+
F: __dxlog_file$1,
|
|
296
|
+
L: 411,
|
|
297
|
+
S: this
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!endHooksFired) {
|
|
302
|
+
endHooksFired = true;
|
|
303
|
+
yield* runEndRequestHooks;
|
|
304
|
+
const after = yield* sessionStore.loadPending(feed);
|
|
305
|
+
if (pendingWork(after)) {
|
|
306
|
+
log("agent work enqueued by end-request hook, continuing", void 0, {
|
|
307
|
+
"~LogMeta": "~LogMeta",
|
|
308
|
+
F: __dxlog_file$1,
|
|
309
|
+
L: 422,
|
|
310
|
+
S: this
|
|
311
|
+
});
|
|
312
|
+
yield* reconcileAlarmWith(after);
|
|
313
|
+
yield* reportSleeping(after);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (asyncHooks.length > 0) {
|
|
318
|
+
log("awaiting background end-request hooks", { count: asyncHooks.length }, {
|
|
135
319
|
"~LogMeta": "~LogMeta",
|
|
136
320
|
F: __dxlog_file$1,
|
|
137
|
-
L:
|
|
321
|
+
L: 432,
|
|
322
|
+
S: this
|
|
323
|
+
});
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (options.resident) {
|
|
327
|
+
log("agent work complete, staying resident", void 0, {
|
|
328
|
+
"~LogMeta": "~LogMeta",
|
|
329
|
+
F: __dxlog_file$1,
|
|
330
|
+
L: 437,
|
|
138
331
|
S: this
|
|
139
332
|
});
|
|
140
|
-
alarmManager.reconcile(true);
|
|
141
333
|
return;
|
|
142
334
|
}
|
|
143
335
|
log("agent work complete, succeeding", void 0, {
|
|
144
336
|
"~LogMeta": "~LogMeta",
|
|
145
337
|
F: __dxlog_file$1,
|
|
146
|
-
L:
|
|
338
|
+
L: 441,
|
|
147
339
|
S: this
|
|
148
340
|
});
|
|
149
341
|
ctx.succeed();
|
|
150
342
|
});
|
|
343
|
+
const maybeComplete = Effect.flatMap(sessionStore.loadPending(feed), maybeCompleteWith);
|
|
151
344
|
return {
|
|
345
|
+
onSpawn: Effect.fnUntraced(function* () {
|
|
346
|
+
if (startupPending.pendingMessages.length > 0) {
|
|
347
|
+
log.info("discarding queue entries left by a previous process", { count: startupPending.pendingMessages.length }, {
|
|
348
|
+
"~LogMeta": "~LogMeta",
|
|
349
|
+
F: __dxlog_file$1,
|
|
350
|
+
L: 454,
|
|
351
|
+
S: this
|
|
352
|
+
});
|
|
353
|
+
yield* Effect.forEach(startupPending.pendingMessages, (message) => sessionStore.ack(feed, message), { discard: true });
|
|
354
|
+
}
|
|
355
|
+
}),
|
|
152
356
|
rpcHandlers: yield* HarnessControl.toHandlers({
|
|
153
357
|
setAlarm: Effect.fn(function* ({ at, message }) {
|
|
154
|
-
yield*
|
|
155
|
-
|
|
358
|
+
const alarm = yield* sessionStore.setAlarm(feed, {
|
|
359
|
+
wakeAt: DateTime.toEpochMillis(at),
|
|
360
|
+
message: message ?? void 0
|
|
361
|
+
});
|
|
362
|
+
unseenAlarms.set(alarm.id, {
|
|
363
|
+
wakeAt: alarm.wakeAt,
|
|
364
|
+
wakes: 0
|
|
365
|
+
});
|
|
366
|
+
yield* reconcileAlarm;
|
|
156
367
|
}),
|
|
157
368
|
enqueueMessage: Effect.fn(function* ({ content }) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
content
|
|
369
|
+
const message = Message.make({
|
|
370
|
+
sender: { role: "user" },
|
|
371
|
+
blocks: [...content]
|
|
161
372
|
});
|
|
162
|
-
yield*
|
|
163
|
-
|
|
373
|
+
yield* sessionStore.enqueueMessage(feed, message);
|
|
374
|
+
unseenWriteIds.add(message.id);
|
|
375
|
+
yield* ctx.setAlarm(0);
|
|
164
376
|
})
|
|
165
377
|
}),
|
|
166
|
-
onInput: Effect.fnUntraced(function* (
|
|
167
|
-
log("agent onInput received", { backlog:
|
|
168
|
-
"~LogMeta": "~LogMeta",
|
|
169
|
-
F: __dxlog_file$1,
|
|
170
|
-
L: 229,
|
|
171
|
-
S: this
|
|
172
|
-
});
|
|
173
|
-
const content = typeof prompt === "string" ? [ContentBlock.Text.make({ text: prompt })] : [...prompt];
|
|
174
|
-
inputQueue.push({
|
|
175
|
-
_tag: "prompt",
|
|
176
|
-
content
|
|
177
|
-
});
|
|
178
|
-
log("agent onInput persisting queue", { depth: inputQueue.length }, {
|
|
378
|
+
onInput: Effect.fnUntraced(function* (input) {
|
|
379
|
+
log("agent onInput received", { backlog: toolResults.length }, {
|
|
179
380
|
"~LogMeta": "~LogMeta",
|
|
180
381
|
F: __dxlog_file$1,
|
|
181
|
-
L:
|
|
382
|
+
L: 483,
|
|
182
383
|
S: this
|
|
183
384
|
});
|
|
184
|
-
|
|
185
|
-
|
|
385
|
+
const message = makeInputMessage(input);
|
|
386
|
+
yield* sessionStore.enqueueMessage(feed, message);
|
|
387
|
+
markWork("agent.prompt-queued");
|
|
388
|
+
unseenWriteIds.add(message.id);
|
|
389
|
+
yield* ctx.setAlarm(0);
|
|
390
|
+
log("agent onInput enqueued to feed", void 0, {
|
|
186
391
|
"~LogMeta": "~LogMeta",
|
|
187
392
|
F: __dxlog_file$1,
|
|
188
|
-
L:
|
|
393
|
+
L: 489,
|
|
189
394
|
S: this
|
|
190
395
|
});
|
|
191
|
-
|
|
192
|
-
|
|
396
|
+
}),
|
|
397
|
+
onAlarm: Effect.fnUntraced(function* () {
|
|
398
|
+
log("agent onAlarm fired", { backlog: toolResults.length }, {
|
|
193
399
|
"~LogMeta": "~LogMeta",
|
|
194
400
|
F: __dxlog_file$1,
|
|
195
|
-
L:
|
|
401
|
+
L: 493,
|
|
196
402
|
S: this
|
|
197
403
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
404
|
+
markWork("agent.wake");
|
|
405
|
+
const announced = toolResults.length > 0 || unseenWriteIds.size > 0;
|
|
406
|
+
if (announced) yield* Trace.emitRequestPhase("preparing");
|
|
407
|
+
for (const pid of dropReportedToolResults(toolResults, (pid) => toolCallManager.isReported(pid))) log.info("skip tool result that was reported synchronously", { pid }, {
|
|
201
408
|
"~LogMeta": "~LogMeta",
|
|
202
409
|
F: __dxlog_file$1,
|
|
203
|
-
L:
|
|
410
|
+
L: 506,
|
|
204
411
|
S: this
|
|
205
412
|
});
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
413
|
+
const toolResult = toolResults.shift();
|
|
414
|
+
let prompt;
|
|
415
|
+
let dequeued;
|
|
416
|
+
if (toolResult !== void 0) {
|
|
417
|
+
log("agent onAlarm handling", { tag: toolResult._tag }, {
|
|
209
418
|
"~LogMeta": "~LogMeta",
|
|
210
419
|
F: __dxlog_file$1,
|
|
211
|
-
L:
|
|
420
|
+
L: 514,
|
|
212
421
|
S: this
|
|
213
422
|
});
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
423
|
+
prompt = toolResultPrompt(toolResult);
|
|
424
|
+
} else {
|
|
425
|
+
const state = yield* sessionStore.loadPending(feed);
|
|
426
|
+
markWork("agent.pending-loaded");
|
|
427
|
+
const stillPending = /* @__PURE__ */ new Set([...state.pendingMessages.map((message) => message.id), ...state.pendingAlarms.map((alarm) => alarm.id)]);
|
|
428
|
+
if (ackedEntries.some((id) => !stillPending.has(id))) {
|
|
429
|
+
ackedEntries = ackedEntries.filter((id) => stillPending.has(id));
|
|
430
|
+
yield* AckedEntriesCell.set(ackedEntries);
|
|
431
|
+
}
|
|
432
|
+
const acked = new Set(ackedEntries);
|
|
433
|
+
const message = state.pendingMessages.find((candidate) => !acked.has(candidate.id));
|
|
434
|
+
const dueAlarm = state.pendingAlarms.find((alarm) => alarm.wakeAt <= now() && !acked.has(alarm.id));
|
|
435
|
+
for (const [id, entry] of unseenAlarms) {
|
|
436
|
+
if (state.pendingAlarms.some((alarm) => alarm.id === id)) continue;
|
|
437
|
+
entry.wakes++;
|
|
438
|
+
if (entry.wakes >= MAX_UNSEEN_WRITE_WAKES) unseenAlarms.delete(id);
|
|
439
|
+
}
|
|
440
|
+
if (message !== void 0) {
|
|
441
|
+
log("agent onAlarm handling", {
|
|
442
|
+
tag: "message",
|
|
443
|
+
id: message.id
|
|
444
|
+
}, {
|
|
445
|
+
"~LogMeta": "~LogMeta",
|
|
446
|
+
F: __dxlog_file$1,
|
|
447
|
+
L: 546,
|
|
448
|
+
S: this
|
|
449
|
+
});
|
|
450
|
+
unseenWriteIds.delete(message.id);
|
|
451
|
+
unseenWriteWakes = 0;
|
|
452
|
+
if (isUserPrompt(message) && selfWakes > 0) {
|
|
453
|
+
selfWakes = 0;
|
|
454
|
+
yield* SelfWakesCell.set(selfWakes);
|
|
455
|
+
}
|
|
456
|
+
dequeued = message;
|
|
457
|
+
prompt = [...message.blocks];
|
|
458
|
+
} else if (dueAlarm !== void 0) {
|
|
459
|
+
unseenAlarms.delete(dueAlarm.id);
|
|
460
|
+
if (selfWakes >= Alarm.MAX_SELF_WAKES) {
|
|
461
|
+
log.warn("agent self-wake budget spent, dropping alarm", { wakes: selfWakes }, {
|
|
462
|
+
"~LogMeta": "~LogMeta",
|
|
463
|
+
F: __dxlog_file$1,
|
|
464
|
+
L: 559,
|
|
465
|
+
S: this
|
|
466
|
+
});
|
|
467
|
+
yield* sessionStore.ack(feed, dueAlarm);
|
|
468
|
+
ackedEntries = [...ackedEntries, dueAlarm.id];
|
|
469
|
+
yield* AckedEntriesCell.set(ackedEntries);
|
|
470
|
+
const after = yield* sessionStore.loadPending(feed);
|
|
471
|
+
yield* reconcileAlarmWith(after);
|
|
472
|
+
yield* maybeCompleteWith(after);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
selfWakes++;
|
|
476
|
+
yield* SelfWakesCell.set(selfWakes);
|
|
477
|
+
log("agent onAlarm self-wake", {
|
|
478
|
+
firedAt: dueAlarm.wakeAt,
|
|
479
|
+
wakes: selfWakes
|
|
480
|
+
}, {
|
|
481
|
+
"~LogMeta": "~LogMeta",
|
|
482
|
+
F: __dxlog_file$1,
|
|
483
|
+
L: 570,
|
|
484
|
+
S: this
|
|
485
|
+
});
|
|
486
|
+
dequeued = dueAlarm;
|
|
487
|
+
prompt = [ContentBlock.Text.make({
|
|
488
|
+
text: wakeUpPrompt(dueAlarm.wakeAt, dueAlarm.message ?? null, {
|
|
489
|
+
wake: selfWakes,
|
|
490
|
+
max: Alarm.MAX_SELF_WAKES
|
|
491
|
+
}),
|
|
492
|
+
disposition: "synthetic"
|
|
493
|
+
})];
|
|
494
|
+
} else if (unseenWriteIds.size > 0 && unseenWriteWakes < MAX_UNSEEN_WRITE_WAKES) {
|
|
495
|
+
unseenWriteWakes++;
|
|
496
|
+
markWork("agent.unseen-write-retry");
|
|
497
|
+
log("agent onAlarm empty queue with an unread write, waking again", {
|
|
498
|
+
unseenWrites: unseenWriteIds.size,
|
|
499
|
+
attempt: unseenWriteWakes
|
|
500
|
+
}, {
|
|
501
|
+
"~LogMeta": "~LogMeta",
|
|
502
|
+
F: __dxlog_file$1,
|
|
503
|
+
L: 588,
|
|
504
|
+
S: this
|
|
505
|
+
});
|
|
506
|
+
yield* ctx.setAlarm(UNSEEN_WRITE_RETRY_MS);
|
|
507
|
+
return;
|
|
508
|
+
} else if (unseenWriteIds.size > 0) {
|
|
509
|
+
log.warn("agent giving up on an unread write, staying resident", {
|
|
510
|
+
unseenWrites: unseenWriteIds.size,
|
|
511
|
+
wakes: unseenWriteWakes
|
|
512
|
+
}, {
|
|
513
|
+
"~LogMeta": "~LogMeta",
|
|
514
|
+
F: __dxlog_file$1,
|
|
515
|
+
L: 599,
|
|
516
|
+
S: this
|
|
517
|
+
});
|
|
518
|
+
yield* reconcileAlarmWith(state);
|
|
519
|
+
return;
|
|
520
|
+
} else {
|
|
521
|
+
log("agent onAlarm empty queue", {}, {
|
|
522
|
+
"~LogMeta": "~LogMeta",
|
|
523
|
+
F: __dxlog_file$1,
|
|
524
|
+
L: 606,
|
|
525
|
+
S: this
|
|
526
|
+
});
|
|
527
|
+
yield* reconcileAlarmWith(state);
|
|
528
|
+
yield* maybeCompleteWith(state);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
220
531
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
S: this
|
|
226
|
-
});
|
|
227
|
-
const item = inputQueue.shift();
|
|
228
|
-
if (!item) {
|
|
229
|
-
log("agent onAlarm empty queue", {}, {
|
|
532
|
+
if (!announced) yield* Trace.emitRequestPhase("preparing");
|
|
533
|
+
const [mcpServers] = yield* Effect.all([loadSpaceMcpServers(), Effect.gen(function* () {
|
|
534
|
+
if (dequeued !== void 0) yield* sessionStore.markInFlight(feed, dequeued);
|
|
535
|
+
log("begin request", { prompt }, {
|
|
230
536
|
"~LogMeta": "~LogMeta",
|
|
231
537
|
F: __dxlog_file$1,
|
|
232
|
-
L:
|
|
538
|
+
L: 629,
|
|
233
539
|
S: this
|
|
234
540
|
});
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
S: this
|
|
245
|
-
});
|
|
246
|
-
const prompt = agentEventToPrompt(item);
|
|
247
|
-
log("begin request", { prompt }, {
|
|
248
|
-
"~LogMeta": "~LogMeta",
|
|
249
|
-
F: __dxlog_file$1,
|
|
250
|
-
L: 267,
|
|
251
|
-
S: this
|
|
252
|
-
});
|
|
253
|
-
log("trace agent request begin", void 0, {
|
|
254
|
-
"~LogMeta": "~LogMeta",
|
|
255
|
-
F: __dxlog_file$1,
|
|
256
|
-
L: 268,
|
|
257
|
-
S: this
|
|
258
|
-
});
|
|
259
|
-
yield* Trace.write(AgentRequestBegin, {});
|
|
541
|
+
log("trace agent request begin", void 0, {
|
|
542
|
+
"~LogMeta": "~LogMeta",
|
|
543
|
+
F: __dxlog_file$1,
|
|
544
|
+
L: 630,
|
|
545
|
+
S: this
|
|
546
|
+
});
|
|
547
|
+
yield* Trace.write(Trace.AgentRequestBegin, {});
|
|
548
|
+
})], { concurrency: "unbounded" });
|
|
549
|
+
markWork("agent.turn-begin");
|
|
260
550
|
yield* session.runTurn({
|
|
261
551
|
prompt,
|
|
552
|
+
sender: dequeued !== void 0 && Obj.instanceOf(Message.Message, dequeued) ? dequeued.sender : void 0,
|
|
262
553
|
system: options.systemPrompt,
|
|
263
|
-
mcpServers
|
|
264
|
-
}).pipe(Effect.onExit((exit) =>
|
|
265
|
-
|
|
266
|
-
|
|
554
|
+
mcpServers
|
|
555
|
+
}).pipe(Effect.onExit((exit) => Effect.gen(function* () {
|
|
556
|
+
yield* Trace.write(Trace.AgentRequestEnd, {
|
|
557
|
+
status: Exit.isSuccess(exit) ? "success" : Exit.hasInterrupts(exit) ? "interrupted" : "error",
|
|
558
|
+
error: Exit.isFailure(exit) ? Cause.pretty(exit.cause) : void 0
|
|
559
|
+
});
|
|
560
|
+
const onTurnFailed = Option.isSome(strategy) ? strategy.value.onTurnFailed : void 0;
|
|
561
|
+
if (onTurnFailed && Exit.isFailure(exit) && !Cause.hasInterrupts(exit.cause)) yield* onTurnFailed(chat, exit.cause);
|
|
267
562
|
})));
|
|
268
563
|
log("end request", void 0, {
|
|
269
564
|
"~LogMeta": "~LogMeta",
|
|
270
565
|
F: __dxlog_file$1,
|
|
271
|
-
L:
|
|
566
|
+
L: 664,
|
|
272
567
|
S: this
|
|
273
568
|
});
|
|
274
|
-
|
|
569
|
+
turnRan = true;
|
|
570
|
+
endHooksFired = false;
|
|
571
|
+
yield* ToolResultsCell.set(toolResults);
|
|
572
|
+
if (dequeued !== void 0) {
|
|
573
|
+
yield* sessionStore.ack(feed, dequeued);
|
|
574
|
+
ackedEntries = [...ackedEntries, dequeued.id];
|
|
575
|
+
yield* AckedEntriesCell.set(ackedEntries);
|
|
576
|
+
}
|
|
577
|
+
const after = yield* sessionStore.loadPending(feed);
|
|
275
578
|
if (Option.isSome(strategy)) {
|
|
276
579
|
const activeIds = new Set(delegations.map((delegation) => delegation.id));
|
|
277
|
-
const pending = yield* strategy.value.reconcile(
|
|
580
|
+
const pending = yield* strategy.value.reconcile(chat, activeIds);
|
|
278
581
|
for (const delegation of pending) {
|
|
279
582
|
const pid = yield* delegation.spawn;
|
|
280
583
|
delegations.push({
|
|
@@ -287,14 +590,18 @@ var AgentProcess = (options) => Process.make({
|
|
|
287
590
|
}, {
|
|
288
591
|
"~LogMeta": "~LogMeta",
|
|
289
592
|
F: __dxlog_file$1,
|
|
290
|
-
L:
|
|
593
|
+
L: 686,
|
|
291
594
|
S: this
|
|
292
595
|
});
|
|
596
|
+
yield* Trace.write(Trace.DelegationSpawned, {
|
|
597
|
+
taskId: delegation.id,
|
|
598
|
+
pid: String(pid)
|
|
599
|
+
});
|
|
293
600
|
}
|
|
294
601
|
if (pending.length > 0) yield* DelegationsCell.set(delegations);
|
|
295
602
|
}
|
|
296
|
-
|
|
297
|
-
yield*
|
|
603
|
+
yield* reconcileAlarmWith(after);
|
|
604
|
+
yield* maybeCompleteWith(after);
|
|
298
605
|
}, Effect.orDie, Effect.provide(Layer.mergeAll(makeToolResolverFromOperations(), ToolExecutionService({
|
|
299
606
|
toolCallManager,
|
|
300
607
|
feed,
|
|
@@ -304,19 +611,40 @@ var AgentProcess = (options) => Process.make({
|
|
|
304
611
|
log("childEvent", { event }, {
|
|
305
612
|
"~LogMeta": "~LogMeta",
|
|
306
613
|
F: __dxlog_file$1,
|
|
307
|
-
L:
|
|
614
|
+
L: 713,
|
|
308
615
|
S: this
|
|
309
616
|
});
|
|
617
|
+
if (event._tag === "exited" && asyncHooks.includes(event.pid)) {
|
|
618
|
+
asyncHooks = asyncHooks.filter((pid) => pid !== event.pid);
|
|
619
|
+
yield* AsyncHooksCell.set(asyncHooks);
|
|
620
|
+
const exit = yield* (yield* Process.ManagerService).attach(event.pid).pipe(Effect.flatMap(Process.awaitOutput), Effect.exit);
|
|
621
|
+
if (Exit.isFailure(exit)) log.warn("background end-request hook failed", { pid: event.pid }, {
|
|
622
|
+
"~LogMeta": "~LogMeta",
|
|
623
|
+
F: __dxlog_file$1,
|
|
624
|
+
L: 721,
|
|
625
|
+
S: this
|
|
626
|
+
});
|
|
627
|
+
yield* maybeComplete;
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
310
630
|
if (event._tag === "exited") {
|
|
311
631
|
const delegation = delegations.find((delegation) => delegation.pid === event.pid);
|
|
312
632
|
if (delegation) {
|
|
313
633
|
delegations = delegations.filter((other) => other.pid !== event.pid);
|
|
314
634
|
yield* DelegationsCell.set(delegations);
|
|
315
|
-
const
|
|
635
|
+
const child = yield* (yield* Process.ManagerService).attach(event.pid);
|
|
636
|
+
const exit = yield* Process.awaitOutput(child).pipe(Effect.exit);
|
|
637
|
+
const result = Exit.isSuccess(exit) ? resultPreview(exit.value) : void 0;
|
|
638
|
+
yield* Trace.write(Trace.DelegationCompleted, {
|
|
639
|
+
taskId: delegation.id,
|
|
640
|
+
pid: String(event.pid),
|
|
641
|
+
status: Exit.isSuccess(exit) ? "success" : "failure",
|
|
642
|
+
...result === void 0 ? {} : { result }
|
|
643
|
+
});
|
|
316
644
|
if (Option.isSome(strategy)) {
|
|
317
|
-
yield* strategy.value.onComplete(
|
|
645
|
+
yield* strategy.value.onComplete(chat, delegation.id, exit);
|
|
318
646
|
const activeIds = new Set(delegations.map((delegation) => delegation.id));
|
|
319
|
-
const pending = yield* strategy.value.reconcile(
|
|
647
|
+
const pending = yield* strategy.value.reconcile(chat, activeIds);
|
|
320
648
|
for (const next of pending) {
|
|
321
649
|
const pid = yield* next.spawn;
|
|
322
650
|
delegations.push({
|
|
@@ -329,9 +657,13 @@ var AgentProcess = (options) => Process.make({
|
|
|
329
657
|
}, {
|
|
330
658
|
"~LogMeta": "~LogMeta",
|
|
331
659
|
F: __dxlog_file$1,
|
|
332
|
-
L:
|
|
660
|
+
L: 757,
|
|
333
661
|
S: this
|
|
334
662
|
});
|
|
663
|
+
yield* Trace.write(Trace.DelegationSpawned, {
|
|
664
|
+
taskId: next.id,
|
|
665
|
+
pid: String(pid)
|
|
666
|
+
});
|
|
335
667
|
}
|
|
336
668
|
if (pending.length > 0) yield* DelegationsCell.set(delegations);
|
|
337
669
|
}
|
|
@@ -342,25 +674,22 @@ var AgentProcess = (options) => Process.make({
|
|
|
342
674
|
}, {
|
|
343
675
|
"~LogMeta": "~LogMeta",
|
|
344
676
|
F: __dxlog_file$1,
|
|
345
|
-
L:
|
|
677
|
+
L: 764,
|
|
346
678
|
S: this
|
|
347
679
|
});
|
|
348
680
|
yield* maybeComplete;
|
|
349
681
|
} else if (toolCallManager.isToolCall(event.pid)) {
|
|
350
|
-
const attachExit = yield* (yield*
|
|
351
|
-
if (Exit.isFailure(attachExit)) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
return yield* Effect.failCause(attachExit.cause).pipe(Effect.orDie);
|
|
682
|
+
const attachExit = yield* (yield* Process.ManagerService).attach(event.pid).pipe(Effect.exit);
|
|
683
|
+
if (Exit.isFailure(attachExit) && isToolResultHandled(toolResults, event.pid, (pid) => toolCallManager.isReported(pid))) {
|
|
684
|
+
log.verbose("childEvent skipped (process gone, result already handled)", { pid: event.pid }, {
|
|
685
|
+
"~LogMeta": "~LogMeta",
|
|
686
|
+
F: __dxlog_file$1,
|
|
687
|
+
L: 775,
|
|
688
|
+
S: this
|
|
689
|
+
});
|
|
690
|
+
return;
|
|
362
691
|
}
|
|
363
|
-
const result = yield* attachExit.
|
|
692
|
+
const result = yield* (Exit.isFailure(attachExit) ? Effect.failCause(attachExit.cause) : Process.awaitOutput(attachExit.value)).pipe(Effect.exit, Effect.map(Exit.match({
|
|
364
693
|
onSuccess: (value) => ({
|
|
365
694
|
_tag: "tool_result",
|
|
366
695
|
pid: event.pid,
|
|
@@ -374,47 +703,43 @@ var AgentProcess = (options) => Process.make({
|
|
|
374
703
|
isError: true
|
|
375
704
|
})
|
|
376
705
|
})));
|
|
377
|
-
|
|
706
|
+
toolResults.push(result);
|
|
378
707
|
log("agent onChildEvent persisted tool result", {
|
|
379
|
-
depth:
|
|
708
|
+
depth: toolResults.length,
|
|
380
709
|
childPid: event.pid
|
|
381
710
|
}, {
|
|
382
711
|
"~LogMeta": "~LogMeta",
|
|
383
712
|
F: __dxlog_file$1,
|
|
384
|
-
L:
|
|
713
|
+
L: 803,
|
|
385
714
|
S: this
|
|
386
715
|
});
|
|
387
|
-
yield*
|
|
388
|
-
|
|
389
|
-
log("agent onChildEvent alarm scheduled", { depth:
|
|
716
|
+
yield* ToolResultsCell.set(toolResults);
|
|
717
|
+
yield* ctx.setAlarm(0);
|
|
718
|
+
log("agent onChildEvent alarm scheduled", { depth: toolResults.length }, {
|
|
390
719
|
"~LogMeta": "~LogMeta",
|
|
391
720
|
F: __dxlog_file$1,
|
|
392
|
-
L:
|
|
721
|
+
L: 806,
|
|
393
722
|
S: this
|
|
394
723
|
});
|
|
395
|
-
} else
|
|
396
|
-
"
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
724
|
+
} else {
|
|
725
|
+
log.verbose("childEvent ignored non-tool call and not a delegation", { pid: event.pid }, {
|
|
726
|
+
"~LogMeta": "~LogMeta",
|
|
727
|
+
F: __dxlog_file$1,
|
|
728
|
+
L: 808,
|
|
729
|
+
S: this
|
|
730
|
+
});
|
|
731
|
+
untrackedExits.add(event.pid);
|
|
732
|
+
}
|
|
401
733
|
}
|
|
402
734
|
})
|
|
403
735
|
};
|
|
404
|
-
}));
|
|
405
|
-
var
|
|
406
|
-
|
|
407
|
-
Schema.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}),
|
|
412
|
-
Schema.TaggedStruct("alarm", {
|
|
413
|
-
firedAt: Schema.Number,
|
|
414
|
-
message: Schema.NullOr(Schema.String)
|
|
415
|
-
})
|
|
416
|
-
]);
|
|
417
|
-
var AgentEventsCell = StorageService.cell(Schema.fromJsonString(Schema.Array(AgentEvent).pipe(Schema.mutable)), "inputQueue").pipe(StorageService.withDefault(() => []));
|
|
736
|
+
})));
|
|
737
|
+
var ToolResultEvent = Schema.TaggedStruct("tool_result", {
|
|
738
|
+
pid: Process.ID,
|
|
739
|
+
result: Schema.Unknown,
|
|
740
|
+
isError: Schema.Boolean
|
|
741
|
+
});
|
|
742
|
+
var ToolResultsCell = StorageService.cell(Schema.fromJsonString(Schema.Array(ToolResultEvent).pipe(Schema.mutable)), "toolResults").pipe(StorageService.withDefault(() => []));
|
|
418
743
|
/**
|
|
419
744
|
* Tracks delegated sub-agent child processes (pid -> correlation id) so that, after a hibernation,
|
|
420
745
|
* a delegated child's exit can be matched back to the work it was fulfilling.
|
|
@@ -424,6 +749,27 @@ var Delegation = Schema.Struct({
|
|
|
424
749
|
id: Schema.String
|
|
425
750
|
}).mapFields(Struct.map(Schema.mutableKey));
|
|
426
751
|
var DelegationsCell = StorageService.cell(Schema.fromJsonString(Schema.Array(Delegation).pipe(Schema.mutable)), "delegations").pipe(StorageService.withDefault(() => []));
|
|
752
|
+
/** Pids of background ({@link Skill.Hook.async}) end-request hooks the process is waiting on. */
|
|
753
|
+
var AsyncHooksCell = StorageService.cell(Schema.fromJsonString(Schema.Array(Process.ID).pipe(Schema.mutable)), "asyncHooks").pipe(StorageService.withDefault(() => []));
|
|
754
|
+
/**
|
|
755
|
+
* Ids of queue entries this process has ACKED, held until the ack is visible in the feed read.
|
|
756
|
+
*
|
|
757
|
+
* The ack is a feed append and a hosted process reads the feed back through the eventually
|
|
758
|
+
* consistent space index, so `loadPending` keeps returning an entry whose ack has not landed yet —
|
|
759
|
+
* and `loadPending` deliberately includes in-flight entries, so that an interrupted turn is
|
|
760
|
+
* redelivered. Without this the two combine into a redelivery loop: the agent re-runs the same turn
|
|
761
|
+
* on every wake, forever. Durable rather than in-memory because the isolate does not survive
|
|
762
|
+
* between turns, and pruned as soon as the entry leaves the pending set, so it cannot grow
|
|
763
|
+
* unbounded.
|
|
764
|
+
*
|
|
765
|
+
* This does NOT weaken at-least-once delivery: an entry is recorded only after its turn ran, which
|
|
766
|
+
* is the same point the ack is written, so a crash before that still redelivers.
|
|
767
|
+
*/
|
|
768
|
+
var AckedEntriesCell = StorageService.cell(Schema.fromJsonString(Schema.Array(Schema.String).pipe(Schema.mutable)), "ackedEntries").pipe(StorageService.withDefault(() => []));
|
|
769
|
+
/** Alarms that have woken the agent since the last user prompt; bounded by {@link Alarm.MAX_SELF_WAKES}. */
|
|
770
|
+
var SelfWakesCell = StorageService.cell(Schema.fromJsonString(Schema.Number), "selfWakes").pipe(StorageService.withDefault(() => 0));
|
|
771
|
+
/** A prompt someone typed, as opposed to one the system wrote (a report, a tool result). */
|
|
772
|
+
var isUserPrompt = (message) => message.sender.role === "user" && message.blocks.some((block) => block._tag === "text" && block.disposition !== "synthetic");
|
|
427
773
|
var ToolCallState = Schema.Struct({ activeCalls: Schema.Array(Schema.Struct({
|
|
428
774
|
pid: Process.ID,
|
|
429
775
|
reported: Schema.Boolean
|
|
@@ -475,7 +821,6 @@ var ToolCallManager = class {
|
|
|
475
821
|
return Effect.gen({ self: this }, function* () {
|
|
476
822
|
let changed = false;
|
|
477
823
|
for (const item of queue) {
|
|
478
|
-
if (item._tag !== "tool_result") continue;
|
|
479
824
|
const call = this.#state.activeCalls.find((entry) => entry.pid === item.pid);
|
|
480
825
|
if (call?.reported) {
|
|
481
826
|
call.reported = false;
|
|
@@ -483,7 +828,7 @@ var ToolCallManager = class {
|
|
|
483
828
|
log("reconcile queued tool result", { pid: item.pid }, {
|
|
484
829
|
"~LogMeta": "~LogMeta",
|
|
485
830
|
F: __dxlog_file$1,
|
|
486
|
-
L:
|
|
831
|
+
L: 971,
|
|
487
832
|
S: this
|
|
488
833
|
});
|
|
489
834
|
}
|
|
@@ -492,8 +837,13 @@ var ToolCallManager = class {
|
|
|
492
837
|
}).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
|
|
493
838
|
}
|
|
494
839
|
};
|
|
495
|
-
/** True while the agent still has queued work, a
|
|
496
|
-
var isAgentWorkPending = ({
|
|
840
|
+
/** True while the agent still has queued work, a pending alarm, subprocesses, or undelivered tool results. */
|
|
841
|
+
var isAgentWorkPending = ({ toolResults, pendingMessages, pendingAlarms, delegations, toolCallManager }) => toolResults.length > 0 || pendingMessages.length > 0 || pendingAlarms.length > 0 || delegations.length > 0 || toolCallManager.hasPendingToolResults();
|
|
842
|
+
/**
|
|
843
|
+
* Whether a tool call's result is already accounted for — queued for the next turn or delivered to the
|
|
844
|
+
* agent — so a finished child that can no longer be reattached needs no answer of its own.
|
|
845
|
+
*/
|
|
846
|
+
var isToolResultHandled = (queue, pid, isReported) => queue.some((item) => item.pid === pid) || isReported(pid);
|
|
497
847
|
/**
|
|
498
848
|
* Discards tool results at the head of the queue whose values already reached the agent.
|
|
499
849
|
*
|
|
@@ -507,37 +857,24 @@ var dropReportedToolResults = (queue, isReported) => {
|
|
|
507
857
|
const dropped = [];
|
|
508
858
|
while (queue.length > 0) {
|
|
509
859
|
const head = queue[0];
|
|
510
|
-
if (
|
|
860
|
+
if (!isReported(head.pid)) break;
|
|
511
861
|
queue.shift();
|
|
512
862
|
dropped.push(head.pid);
|
|
513
863
|
}
|
|
514
864
|
return dropped;
|
|
515
865
|
};
|
|
516
866
|
/**
|
|
517
|
-
* Renders a
|
|
518
|
-
*
|
|
519
|
-
* A tool result recovered across a reload is redelivered as a synthetic `<result pid=N>` TEXT block
|
|
867
|
+
* Renders a recovered tool result as the next turn's prompt: a synthetic `<result pid=N>` TEXT block
|
|
520
868
|
* rather than a tool-result part, because the request it belonged to is gone and its tool-call id
|
|
521
869
|
* cannot be answered. `disposition: 'synthetic'` keeps it out of the user-visible transcript.
|
|
522
870
|
*/
|
|
523
|
-
var
|
|
524
|
-
text: event.isError ? toolErrorResponse(event.pid, event.result) : toolResultResponse(event.pid, event.result),
|
|
525
|
-
disposition: "synthetic"
|
|
526
|
-
})]), Match.tag("alarm", (event) => [ContentBlock.Text.make({
|
|
527
|
-
text: wakeUpPrompt(event.firedAt, event.message),
|
|
871
|
+
var toolResultPrompt = (event) => [ContentBlock.Text.make({
|
|
872
|
+
text: event.isError ? toolErrorResponse(event.pid, String(event.result)) : toolResultResponse(event.pid, event.result),
|
|
528
873
|
disposition: "synthetic"
|
|
529
|
-
})]
|
|
874
|
+
})];
|
|
530
875
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*/
|
|
534
|
-
var AgentAlarmCell = StorageService.cell(Schema.fromJsonString(Schema.NullOr(Schema.Struct({
|
|
535
|
-
wakeAt: Schema.Number,
|
|
536
|
-
message: Schema.NullOr(Schema.String)
|
|
537
|
-
}))), "agentAlarm").pipe(StorageService.withDefault(() => null));
|
|
538
|
-
/**
|
|
539
|
-
* Computes the timeout to pass to `ctx.setAlarm`, reconciling pending queue work with the agent's
|
|
540
|
-
* self-wake alarm. Returns `null` when no alarm should be scheduled (process can go idle).
|
|
876
|
+
* Computes the timeout to pass to `ctx.setAlarm`, reconciling pending queue work with the earliest
|
|
877
|
+
* pending feed alarm. Returns `null` when no alarm should be scheduled (process can go idle).
|
|
541
878
|
*/
|
|
542
879
|
var computeAlarmDelay = ({ hasPendingWork, wakeAt, now }) => {
|
|
543
880
|
if (hasPendingWork) return 0;
|
|
@@ -545,96 +882,19 @@ var computeAlarmDelay = ({ hasPendingWork, wakeAt, now }) => {
|
|
|
545
882
|
return null;
|
|
546
883
|
};
|
|
547
884
|
/**
|
|
548
|
-
* Tracks the next agent-scheduled self-wake and keeps it in sync with the process alarm
|
|
549
|
-
* (`ctx.setAlarm`). The agent sets alarms via the alarm blueprint, which dispatches to this manager
|
|
550
|
-
* through the `HarnessControl` RPC surface; the process reconciles the underlying single-shot alarm
|
|
551
|
-
* timer to fire at the earliest of pending work or the self-wake.
|
|
552
|
-
*/
|
|
553
|
-
var AlarmManager = class {
|
|
554
|
-
#storageService;
|
|
555
|
-
#setAlarm;
|
|
556
|
-
#now;
|
|
557
|
-
#wakeAt = null;
|
|
558
|
-
#message = null;
|
|
559
|
-
constructor({ storageService, setAlarm, now = () => Date.now() }) {
|
|
560
|
-
this.#storageService = storageService;
|
|
561
|
-
this.#setAlarm = setAlarm;
|
|
562
|
-
this.#now = now;
|
|
563
|
-
}
|
|
564
|
-
/** Currently scheduled self-wake timestamp (ms), or `null` when none is set. */
|
|
565
|
-
get wakeAt() {
|
|
566
|
-
return this.#wakeAt;
|
|
567
|
-
}
|
|
568
|
-
/** Reminder message delivered when the current self-wake fires, or `null` when none is set. */
|
|
569
|
-
get message() {
|
|
570
|
-
return this.#message;
|
|
571
|
-
}
|
|
572
|
-
now() {
|
|
573
|
-
return this.#now();
|
|
574
|
-
}
|
|
575
|
-
/** Restores the persisted alarm state. */
|
|
576
|
-
load() {
|
|
577
|
-
return Effect.gen({ self: this }, function* () {
|
|
578
|
-
const persisted = yield* AgentAlarmCell.get;
|
|
579
|
-
this.#wakeAt = persisted?.wakeAt ?? null;
|
|
580
|
-
this.#message = persisted?.message ?? null;
|
|
581
|
-
}).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
|
|
582
|
-
}
|
|
583
|
-
/** Records a new self-wake target (with an optional reminder message) and persists it. */
|
|
584
|
-
setWakeAt(wakeAt, message = null) {
|
|
585
|
-
return Effect.gen({ self: this }, function* () {
|
|
586
|
-
this.#wakeAt = wakeAt;
|
|
587
|
-
this.#message = message;
|
|
588
|
-
yield* AgentAlarmCell.set({
|
|
589
|
-
wakeAt,
|
|
590
|
-
message
|
|
591
|
-
});
|
|
592
|
-
}).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Clears the self-wake alarm if it is due, returning the timestamp it was scheduled for and the
|
|
596
|
-
* reminder message it carried (or `null` if no alarm was due).
|
|
597
|
-
*/
|
|
598
|
-
takeFiredAlarm() {
|
|
599
|
-
return Effect.gen({ self: this }, function* () {
|
|
600
|
-
if (this.#wakeAt == null || this.#now() < this.#wakeAt) return null;
|
|
601
|
-
const firedAt = this.#wakeAt;
|
|
602
|
-
const message = this.#message;
|
|
603
|
-
this.#wakeAt = null;
|
|
604
|
-
this.#message = null;
|
|
605
|
-
yield* AgentAlarmCell.set(null);
|
|
606
|
-
return {
|
|
607
|
-
firedAt,
|
|
608
|
-
message
|
|
609
|
-
};
|
|
610
|
-
}).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Reconciles the process alarm timer with pending work and the tracked self-wake, scheduling the
|
|
614
|
-
* earliest of the two. Does nothing when there is neither pending work nor a self-wake.
|
|
615
|
-
*/
|
|
616
|
-
reconcile(hasPendingWork) {
|
|
617
|
-
const delay = computeAlarmDelay({
|
|
618
|
-
hasPendingWork,
|
|
619
|
-
wakeAt: this.#wakeAt,
|
|
620
|
-
now: this.#now()
|
|
621
|
-
});
|
|
622
|
-
if (delay != null) this.#setAlarm(delay);
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
/**
|
|
626
885
|
* Prompt delivered to the agent when a self-scheduled alarm fires. When the alarm carried a
|
|
627
886
|
* reminder message it is surfaced verbatim, otherwise a generic continuation prompt is used.
|
|
887
|
+
* Exported so the prompt shape stays pinned by tests without spawning an agent.
|
|
628
888
|
*/
|
|
629
|
-
var wakeUpPrompt = (firedAt, message) =>
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
889
|
+
var wakeUpPrompt = (firedAt, message, budget) => {
|
|
890
|
+
return [
|
|
891
|
+
`Scheduled alarm fired (it was set for ${new Date(firedAt).toISOString()}).`,
|
|
892
|
+
message ?? "Continue with whatever you intended to do when you scheduled this wake-up.",
|
|
893
|
+
budget == null ? void 0 : budget.wake >= budget.max ? `This is self-wake ${budget.wake} of ${budget.max}: further alarms will not wake you until the user writes again, so finish or report where you are now.` : `This is self-wake ${budget.wake} of ${budget.max} before the user must write again.`
|
|
894
|
+
].filter((line) => line != null).join("\n");
|
|
895
|
+
};
|
|
636
896
|
var ToolExecutionService = ({ enableBackgrounding, backgroundThreshold = Duration.seconds(1), toolCallManager, feed }) => Layer.unwrap(Effect.gen(function* () {
|
|
637
|
-
const
|
|
897
|
+
const childServices = yield* Effect.context();
|
|
638
898
|
return makeToolExecutionService({ invoke: (tool, input) => Effect.gen(function* () {
|
|
639
899
|
const operationDef = getOperationFromTool(tool).pipe(Option.getOrThrow);
|
|
640
900
|
log("invoking operation", {
|
|
@@ -643,34 +903,37 @@ var ToolExecutionService = ({ enableBackgrounding, backgroundThreshold = Duratio
|
|
|
643
903
|
}, {
|
|
644
904
|
"~LogMeta": "~LogMeta",
|
|
645
905
|
F: __dxlog_file$1,
|
|
646
|
-
L:
|
|
906
|
+
L: 1117,
|
|
647
907
|
S: this
|
|
648
908
|
});
|
|
649
|
-
const
|
|
909
|
+
const handle = yield* Process.spawn(OperationProcess.make(operationDef), input, {
|
|
650
910
|
environment: { conversation: Obj.getURI(feed) },
|
|
651
911
|
traceMeta: { conversation: Ref.make(feed) }
|
|
652
912
|
});
|
|
653
|
-
|
|
913
|
+
markWork("tool.spawned");
|
|
914
|
+
yield* toolCallManager.beginCall(handle.pid);
|
|
915
|
+
markWork("tool.call-recorded");
|
|
654
916
|
log("invoked operation", {
|
|
655
917
|
operationDef,
|
|
656
918
|
input,
|
|
657
|
-
|
|
919
|
+
pid: handle.pid
|
|
658
920
|
}, {
|
|
659
921
|
"~LogMeta": "~LogMeta",
|
|
660
922
|
F: __dxlog_file$1,
|
|
661
|
-
L:
|
|
923
|
+
L: 1129,
|
|
662
924
|
S: this
|
|
663
925
|
});
|
|
664
|
-
const awaitWithReport =
|
|
665
|
-
const result = enableBackgrounding ? yield* awaitWithReport.pipe(Effect.timeout(backgroundThreshold), Effect.catchTag("TimeoutError", () => Effect.succeed(Exit.succeed(toolIsRunningInBackgroundResponse(
|
|
926
|
+
const awaitWithReport = Process.awaitOutput(handle).pipe(Effect.exit, Effect.tap(() => toolCallManager.markAsReported(handle.pid)));
|
|
927
|
+
const result = enableBackgrounding ? yield* awaitWithReport.pipe(Effect.timeout(backgroundThreshold), Effect.catchTag("TimeoutError", () => Effect.succeed(Exit.succeed(toolIsRunningInBackgroundResponse(handle.pid))))) : yield* awaitWithReport;
|
|
928
|
+
markWork("tool.settled");
|
|
666
929
|
log("result", { result }, {
|
|
667
930
|
"~LogMeta": "~LogMeta",
|
|
668
931
|
F: __dxlog_file$1,
|
|
669
|
-
L:
|
|
932
|
+
L: 1144,
|
|
670
933
|
S: this
|
|
671
934
|
});
|
|
672
935
|
return yield* result;
|
|
673
|
-
}) });
|
|
936
|
+
}).pipe(Effect.provide(childServices)) });
|
|
674
937
|
}));
|
|
675
938
|
var AsynchronousExectionToolkit = class extends Toolkit.make(Tool.make("poll-tools", {
|
|
676
939
|
description: trim`
|
|
@@ -693,12 +956,16 @@ var AsynchronousExectionToolkit = class extends Toolkit.make(Tool.make("poll-too
|
|
|
693
956
|
})
|
|
694
957
|
})) {};
|
|
695
958
|
var AsynchronousExectionToolkitLayer = AsynchronousExectionToolkit.toLayer(Effect.gen(function* () {
|
|
696
|
-
const
|
|
959
|
+
const manager = yield* Process.ManagerService;
|
|
697
960
|
return { "poll-tools": ({ ids, wait, timeout = 1e4 }) => Effect.gen(function* () {
|
|
698
|
-
return yield* Effect.forEach(ids, (pid) =>
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
961
|
+
return yield* Effect.forEach(ids, (pid) => Effect.gen(function* () {
|
|
962
|
+
const attached = yield* manager.attach(Process.ID.make(pid)).pipe(Effect.exit);
|
|
963
|
+
if (Exit.isFailure(attached)) return `Process not found: ${pid}`;
|
|
964
|
+
return yield* Process.awaitOutput(attached.value).pipe(Effect.exit, Effect.timeout(Duration.millis(timeout)), Effect.map(Exit.match({
|
|
965
|
+
onSuccess: (value) => toolResultResponse(pid, value),
|
|
966
|
+
onFailure: (cause) => toolErrorResponse(pid, Cause.pretty(cause))
|
|
967
|
+
})), Effect.catchTag("TimeoutError", () => Effect.succeed(`Process still running: ${pid}`)));
|
|
968
|
+
}));
|
|
702
969
|
}) };
|
|
703
970
|
}));
|
|
704
971
|
/**
|
|
@@ -714,9 +981,11 @@ var AgentService_exports = /* @__PURE__ */ __exportAll({
|
|
|
714
981
|
layer: () => layer
|
|
715
982
|
});
|
|
716
983
|
var __dxlog_file = "/__w/dxos/dxos/packages/core/compute/agent-runtime/src/agent-service/AgentService.ts";
|
|
717
|
-
|
|
984
|
+
/**
|
|
985
|
+
* Creates a session on a fresh feed and chat, with `opts.skills` and `opts.context` bound to it.
|
|
986
|
+
*/
|
|
718
987
|
var createSession = Effect.fn("createSession")(function* (opts) {
|
|
719
|
-
const skills =
|
|
988
|
+
const skills = (opts?.skills ?? []).map(Skill.makeRef);
|
|
720
989
|
const feed = yield* Database.add(Feed.make());
|
|
721
990
|
const runtime = yield* Effect.context();
|
|
722
991
|
const binder = yield* EffectEx.acquireReleaseResource(() => new AiContext.Binder({
|
|
@@ -727,101 +996,182 @@ var createSession = Effect.fn("createSession")(function* (opts) {
|
|
|
727
996
|
skills,
|
|
728
997
|
objects: opts?.context ?? []
|
|
729
998
|
}));
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
});
|
|
999
|
+
const chat = yield* Database.add(Chat.make({
|
|
1000
|
+
feed: Ref.make(feed),
|
|
1001
|
+
...opts?.model ? { session: { model: opts.model } } : {}
|
|
1002
|
+
}));
|
|
1003
|
+
return yield* AgentService.getSession(chat, { provider: opts?.provider });
|
|
734
1004
|
}, Effect.scoped);
|
|
735
|
-
|
|
736
|
-
|
|
1005
|
+
/**
|
|
1006
|
+
* The `AgentService` layer.
|
|
1007
|
+
*
|
|
1008
|
+
* Requires {@link Process.ManagerService}, which runs a session locally or, for `location: 'edge'`,
|
|
1009
|
+
* on the remote runtime. A host without EDGE satisfies its remote half with
|
|
1010
|
+
* `RemoteProcessManager.layerNoop`, so an edge session fails at spawn rather than silently running
|
|
1011
|
+
* locally.
|
|
1012
|
+
*/
|
|
1013
|
+
var layer = (opts) => Layer.effect(AgentService.AgentService, Effect.gen(function* () {
|
|
1014
|
+
const processManager = yield* Process.ManagerService;
|
|
1015
|
+
const remoteSpaces = /* @__PURE__ */ new Set();
|
|
1016
|
+
/**
|
|
1017
|
+
* Where a session's agent runs. `edge` needs the space, since one remote runtime spans them,
|
|
1018
|
+
* and a chat with no space cannot name where its agent would run.
|
|
1019
|
+
*/
|
|
1020
|
+
const locationFor = (location, spaceId) => {
|
|
1021
|
+
if (location !== "edge") return { kind: "local" };
|
|
1022
|
+
if (!spaceId) throw new Error("Agent requested on edge, but its conversation has no space.");
|
|
1023
|
+
remoteSpaces.add(spaceId);
|
|
1024
|
+
return {
|
|
1025
|
+
kind: "edge",
|
|
1026
|
+
space: spaceId
|
|
1027
|
+
};
|
|
1028
|
+
};
|
|
737
1029
|
const sessionCache = /* @__PURE__ */ new Map();
|
|
738
|
-
const
|
|
1030
|
+
const sessionLocks = /* @__PURE__ */ new Map();
|
|
1031
|
+
const lockFor = (chatId) => {
|
|
1032
|
+
const existing = sessionLocks.get(chatId);
|
|
1033
|
+
if (existing) return existing;
|
|
1034
|
+
const lock = Effect.runSync(Semaphore.make(1));
|
|
1035
|
+
sessionLocks.set(chatId, lock);
|
|
1036
|
+
return lock;
|
|
1037
|
+
};
|
|
1038
|
+
const makeExecutable = (provider) => AgentProcess({
|
|
739
1039
|
systemPrompt: opts?.systemPrompt,
|
|
740
1040
|
makeTurnProducer: opts?.makeTurnProducer,
|
|
741
|
-
|
|
1041
|
+
defaultModel: opts?.defaultModel,
|
|
742
1042
|
provider: provider ?? opts?.provider,
|
|
743
|
-
getMcpServers: opts?.getMcpServers,
|
|
744
1043
|
enableToolBackgrounding: opts?.enableToolBackgrounding,
|
|
745
|
-
delegationStrategy: opts?.delegationStrategy
|
|
1044
|
+
delegationStrategy: opts?.delegationStrategy,
|
|
1045
|
+
resident: opts?.resident
|
|
746
1046
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* The process a chat runs on: the one its session names, or ours. A name nothing contributed
|
|
1049
|
+
* (its plugin was removed) falls back to ours rather than leaving the chat unable to run.
|
|
1050
|
+
*/
|
|
1051
|
+
const executableFor = (chat, provider) => {
|
|
1052
|
+
const key = chat.session?.process;
|
|
1053
|
+
if (key === void 0 || key === "org.dxos.testing.process.agent") return makeExecutable(provider);
|
|
1054
|
+
const definition = opts?.processes?.().find((candidate) => candidate.key === key);
|
|
1055
|
+
if (!definition) {
|
|
1056
|
+
log.warn("agent process not contributed; running the default", { key }, {
|
|
1057
|
+
"~LogMeta": "~LogMeta",
|
|
1058
|
+
F: __dxlog_file,
|
|
1059
|
+
L: 201,
|
|
1060
|
+
S: this
|
|
1061
|
+
});
|
|
1062
|
+
return makeExecutable(provider);
|
|
1063
|
+
}
|
|
1064
|
+
return definition;
|
|
1065
|
+
};
|
|
1066
|
+
const service = {
|
|
1067
|
+
getSession: (chat, options) => Effect.suspend(() => lockFor(chat.id).withPermits(1)(Effect.gen(function* () {
|
|
750
1068
|
const provider = options?.provider ?? opts?.provider;
|
|
751
|
-
const
|
|
752
|
-
const
|
|
1069
|
+
const model = chat.session?.model;
|
|
1070
|
+
const instructions = chat.instructions?.uri;
|
|
1071
|
+
const location = options?.location ?? "local";
|
|
1072
|
+
const executable = executableFor(chat, provider);
|
|
1073
|
+
if (location === "edge" && executable.key !== "org.dxos.testing.process.agent") return yield* Effect.die(/* @__PURE__ */ new Error(`Agent process ${executable.key} runs only locally, but the chat asked for edge.`));
|
|
1074
|
+
const cached = sessionCache.get(chat.id);
|
|
753
1075
|
if (cached) {
|
|
754
|
-
if (cached.model === model && cached.provider === provider && cached.instructions === instructions && !
|
|
755
|
-
if (!
|
|
756
|
-
sessionCache.delete(
|
|
1076
|
+
if (cached.model === model && cached.provider === provider && cached.instructions === instructions && cached.location === location && cached.process === executable.key && !Process.isTerminal(cached.handle.status.state)) return cached.session;
|
|
1077
|
+
if (!Process.isTerminal(cached.handle.status.state)) yield* cached.handle.terminate();
|
|
1078
|
+
sessionCache.delete(chat.id);
|
|
757
1079
|
}
|
|
758
|
-
const
|
|
1080
|
+
const feed = yield* Database.load(chat.feed).pipe(Effect.orDie);
|
|
1081
|
+
const target = Obj.getURI(chat);
|
|
759
1082
|
const parsedEchoUri = EID.tryParse(target);
|
|
760
1083
|
const spaceId = parsedEchoUri ? EID.getSpaceId(parsedEchoUri) : void 0;
|
|
761
|
-
const
|
|
762
|
-
let activeProcess = (yield* processManager.
|
|
1084
|
+
const processLocation = locationFor(options?.location, spaceId);
|
|
1085
|
+
let activeProcess = (yield* processManager.handles({
|
|
763
1086
|
target,
|
|
764
|
-
key: executable.key
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (Option.getOrUndefined(Annotation.getDictionary(activeProcess.params.annotations, Process.InstructionsAnnotation)) !== instructions) {
|
|
768
|
-
yield* activeProcess.terminate();
|
|
769
|
-
activeProcess = void 0;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
1087
|
+
key: executable.key,
|
|
1088
|
+
location: processLocation
|
|
1089
|
+
})).find((process) => !Process.isTerminal(process.status.state));
|
|
772
1090
|
let handle;
|
|
773
|
-
if (activeProcess)
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
} else handle = yield* processManager.spawn(executable, {
|
|
1091
|
+
if (activeProcess) handle = yield* activeProcess.hydrate(executable);
|
|
1092
|
+
else handle = yield* processManager.spawn(executable, {
|
|
1093
|
+
location: processLocation,
|
|
777
1094
|
name: "Agent",
|
|
778
1095
|
target,
|
|
779
1096
|
annotations: Annotation.buildDictionary((dictionary) => {
|
|
780
1097
|
Annotation.setDictionary(dictionary, Process.HarnessHostAnnotation, true);
|
|
781
|
-
if (options?.instructions) Annotation.setDictionary(dictionary, Process.InstructionsAnnotation, options.instructions.uri);
|
|
782
1098
|
}),
|
|
783
1099
|
environment: {
|
|
784
1100
|
...spaceId !== void 0 ? { space: spaceId } : {},
|
|
785
|
-
conversation:
|
|
1101
|
+
conversation: Obj.getURI(feed)
|
|
786
1102
|
},
|
|
787
1103
|
traceMeta: { conversation: Ref.make(feed) }
|
|
788
1104
|
});
|
|
789
1105
|
const releaseSession = () => {
|
|
790
|
-
sessionCache.delete(
|
|
1106
|
+
sessionCache.delete(chat.id);
|
|
791
1107
|
};
|
|
792
|
-
const
|
|
793
|
-
|
|
1108
|
+
const databaseContext = yield* Effect.context();
|
|
1109
|
+
const isFinished = Effect.suspend(() => Process.isTerminal(handle.status.state) || location !== "edge" ? Effect.succeed(Process.isTerminal(handle.status.state)) : processManager.handles({
|
|
1110
|
+
target,
|
|
1111
|
+
key: executable.key,
|
|
1112
|
+
location: processLocation
|
|
1113
|
+
}).pipe(Effect.map((live) => {
|
|
1114
|
+
const current = live.find((process) => process.pid === handle.pid);
|
|
1115
|
+
return current === void 0 || Process.isTerminal(current.status.state);
|
|
1116
|
+
}), Effect.orElseSucceed(() => false)));
|
|
1117
|
+
const resubmit = (prompt, submitOptions) => Effect.sync(releaseSession).pipe(Effect.andThen(service.getSession(chat, options)), Effect.flatMap((next) => next.submitPrompt(prompt, submitOptions)), Effect.provide(databaseContext));
|
|
1118
|
+
const session = makeSession(handle, chat, feed, releaseSession, isFinished, resubmit);
|
|
1119
|
+
sessionCache.set(chat.id, {
|
|
794
1120
|
model,
|
|
795
1121
|
provider,
|
|
796
1122
|
instructions,
|
|
1123
|
+
location,
|
|
1124
|
+
process: executable.key,
|
|
797
1125
|
handle,
|
|
798
1126
|
session
|
|
799
1127
|
});
|
|
800
1128
|
return session;
|
|
801
|
-
}),
|
|
1129
|
+
}))),
|
|
802
1130
|
hydrate: Effect.fnUntraced(function* () {
|
|
803
1131
|
sessionCache.clear();
|
|
1132
|
+
const contributed = (opts?.processes?.() ?? []).filter(({ key }) => key !== AGENT_PROCESS_KEY);
|
|
804
1133
|
const executable = makeExecutable();
|
|
805
|
-
const agents =
|
|
1134
|
+
const agents = [
|
|
1135
|
+
...(yield* processManager.handles({ key: AGENT_PROCESS_KEY })).map((agent) => ({
|
|
1136
|
+
agent,
|
|
1137
|
+
executable
|
|
1138
|
+
})),
|
|
1139
|
+
...(yield* Effect.forEach(contributed, (definition) => processManager.handles({ key: definition.key }).pipe(Effect.map((agents) => agents.map((agent) => ({
|
|
1140
|
+
agent,
|
|
1141
|
+
executable: definition
|
|
1142
|
+
})))))).flat(),
|
|
1143
|
+
...(yield* Effect.forEach([...remoteSpaces], (space) => processManager.handles({
|
|
1144
|
+
key: AGENT_PROCESS_KEY,
|
|
1145
|
+
location: {
|
|
1146
|
+
kind: "edge",
|
|
1147
|
+
space
|
|
1148
|
+
}
|
|
1149
|
+
}))).flat().map((agent) => ({
|
|
1150
|
+
agent,
|
|
1151
|
+
executable
|
|
1152
|
+
}))
|
|
1153
|
+
];
|
|
806
1154
|
log("agent hydrate", { count: agents.length }, {
|
|
807
1155
|
"~LogMeta": "~LogMeta",
|
|
808
1156
|
F: __dxlog_file,
|
|
809
|
-
L:
|
|
1157
|
+
L: 231,
|
|
810
1158
|
S: this
|
|
811
1159
|
});
|
|
812
|
-
for (const agent of agents) yield* agent.hydrate(executable).pipe(Effect.catchCause((cause) => Effect.sync(() => log.warn("agent hydrate skipped", {
|
|
1160
|
+
for (const { agent, executable } of agents) yield* agent.hydrate(executable).pipe(Effect.catchCause((cause) => Effect.sync(() => log.warn("agent hydrate skipped", {
|
|
813
1161
|
pid: agent.pid,
|
|
814
1162
|
cause: Cause.pretty(cause)
|
|
815
1163
|
}, {
|
|
816
1164
|
"~LogMeta": "~LogMeta",
|
|
817
1165
|
F: __dxlog_file,
|
|
818
|
-
L:
|
|
1166
|
+
L: 237,
|
|
819
1167
|
S: this
|
|
820
1168
|
}))));
|
|
821
1169
|
})
|
|
822
1170
|
};
|
|
1171
|
+
return service;
|
|
823
1172
|
}));
|
|
824
|
-
var makeSession = (process, feed, releaseSession) => ({
|
|
1173
|
+
var makeSession = (process, chat, feed, releaseSession, isFinished, resubmit) => ({
|
|
1174
|
+
chat,
|
|
825
1175
|
feed,
|
|
826
1176
|
getContext: () => Effect.gen(function* () {
|
|
827
1177
|
const runtime = yield* Effect.context();
|
|
@@ -841,12 +1191,16 @@ var makeSession = (process, feed, releaseSession) => ({
|
|
|
841
1191
|
objects: context
|
|
842
1192
|
}));
|
|
843
1193
|
}).pipe(Effect.scoped),
|
|
844
|
-
submitPrompt: (prompt) => process.submitInput(
|
|
1194
|
+
submitPrompt: (prompt, options) => Effect.flatMap(isFinished, (finished) => finished ? resubmit(prompt, options) : process.submitInput(options?.sender ? {
|
|
1195
|
+
prompt,
|
|
1196
|
+
sender: options.sender
|
|
1197
|
+
} : prompt)),
|
|
1198
|
+
running: Atom.make((get) => get(process.statusAtom).state === Process.State.RUNNING || get(process.statusAtom).state === Process.State.HYBERNATING),
|
|
845
1199
|
waitForCompletion: () => process.runUntilSettled(),
|
|
846
1200
|
terminate: () => process.terminate().pipe(Effect.tap(() => Effect.sync(releaseSession))),
|
|
847
|
-
subscribeEphemeral: () => process.subscribeEphemeral()
|
|
1201
|
+
subscribeEphemeral: (options) => process.subscribeEphemeral(options)
|
|
848
1202
|
});
|
|
849
1203
|
//#endregion
|
|
850
|
-
export {
|
|
1204
|
+
export { AgentProcess as a, __exportAll as c, AgentInput as i, layer as n, makeInputMessage as o, AGENT_PROCESS_KEY as r, makeAiSessionTurnProducer as s, AgentService_exports as t };
|
|
851
1205
|
|
|
852
1206
|
//# sourceMappingURL=chunk-AgentService.mjs.map
|