@frockbot/plugin-shell 0.0.0 → 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/frockbot.json +68 -0
- package/package.json +87 -6
- package/src/agent.test.ts +372 -0
- package/src/agent.ts +335 -0
- package/src/approvals.test.ts +224 -0
- package/src/approvals.ts +530 -0
- package/src/backend-assignment.test.ts +161 -0
- package/src/backend-assignment.ts +274 -0
- package/src/backend-authoring.test.ts +518 -0
- package/src/backend-authoring.ts +531 -0
- package/src/backend-bot-identity.test.ts +215 -0
- package/src/backend-completion.test.ts +289 -0
- package/src/backend-completion.ts +95 -0
- package/src/backend-composition.ts +242 -0
- package/src/backend-computer.ts +76 -0
- package/src/backend-configuration.test.ts +1757 -0
- package/src/backend-contracts.test.ts +189 -0
- package/src/backend-contracts.ts +44 -0
- package/src/backend-debug.test.ts +202 -0
- package/src/backend-execution.ts +55 -0
- package/src/backend-flock.ts +96 -0
- package/src/backend-image.test.ts +115 -0
- package/src/backend-image.ts +180 -0
- package/src/backend-isolate.test.ts +238 -0
- package/src/backend-isolate.ts +409 -0
- package/src/backend-machine.ts +144 -0
- package/src/backend-memory.ts +89 -0
- package/src/backend-recovery-integration.test.ts +1575 -0
- package/src/backend-recovery.ts +106 -0
- package/src/backend-routines.ts +375 -0
- package/src/backend-runner.ts +251 -0
- package/src/backend-skills.test.ts +126 -0
- package/src/backend-skills.ts +198 -0
- package/src/backend-stop.test.ts +356 -0
- package/src/backend-subagents.ts +459 -0
- package/src/backend.ts +6035 -0
- package/src/client/FrockBotApp.vue +1026 -0
- package/src/client/SendPayloadView.vue +337 -0
- package/src/client/composer-draft.test.ts +31 -0
- package/src/client/composer-draft.ts +35 -0
- package/src/client/cordis-client-shim.d.ts +15 -0
- package/src/client/index.test.ts +2548 -0
- package/src/client/index.ts +2346 -0
- package/src/client/model-presentation.test.ts +35 -0
- package/src/client/model-presentation.ts +19 -0
- package/src/client/notify.test.ts +89 -0
- package/src/client/notify.ts +101 -0
- package/src/client/skill-invocation.test.ts +143 -0
- package/src/client/skill-invocation.ts +175 -0
- package/src/client/styles.css +1043 -0
- package/src/composition-views.ts +118 -0
- package/src/debug-protocol.test.ts +80 -0
- package/src/debug-protocol.ts +165 -0
- package/src/env.d.ts +10 -0
- package/src/history.test.ts +163 -0
- package/src/history.ts +108 -0
- package/src/host.ts +20 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/run-cursor.ts +28 -0
- package/src/run-protocol.test.ts +1281 -0
- package/src/run-protocol.ts +1417 -0
- package/src/settings-links.test.ts +106 -0
- package/src/settings-links.ts +289 -0
- package/src/shared.ts +338 -0
- package/src/skill-protocol.ts +117 -0
- package/src/terminal-records.test.ts +217 -0
- package/src/terminal-records.ts +150 -0
- package/src/unread.test.ts +362 -0
- package/src/unread.ts +675 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +32 -0
- package/README.md +0 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Kernel run-recovery planning, bound to the Shell Package's run codec.
|
|
2
|
+
import {
|
|
3
|
+
Session,
|
|
4
|
+
type SessionEvent,
|
|
5
|
+
validateToolOccurrenceJournal,
|
|
6
|
+
} from "@frockbot/kernel-contracts";
|
|
7
|
+
import {
|
|
8
|
+
latestModelRequestJournalState,
|
|
9
|
+
planBotRunRecovery as planKernelBotRunRecovery,
|
|
10
|
+
type BotRunRecoveryPlan,
|
|
11
|
+
} from "@frockbot/kernel-do";
|
|
12
|
+
import {
|
|
13
|
+
requireStoredRunV1,
|
|
14
|
+
storedRunCodecV1,
|
|
15
|
+
type StoredRun,
|
|
16
|
+
} from "./backend-contracts.js";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
eventsForFailedRun,
|
|
20
|
+
latestModelRequestJournalState,
|
|
21
|
+
type BotRunRecoveryPlan,
|
|
22
|
+
type ModelRequestJournalState,
|
|
23
|
+
} from "@frockbot/kernel-do";
|
|
24
|
+
|
|
25
|
+
/** How a stopped run's unresolved effects settle. */
|
|
26
|
+
export type StoppedRunRecoveryPlan =
|
|
27
|
+
{ kind: "cancel"; events: SessionEvent[] } | { kind: "reconcile" };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Classifies the unresolved effects of a stopped run from their durable
|
|
31
|
+
* admission records. With no compatibility data, a missing admission is the
|
|
32
|
+
* same definitive no-start outcome as `fenced`; only explicit `admitted`
|
|
33
|
+
* remains uncertain.
|
|
34
|
+
*/
|
|
35
|
+
export function planStoppedRunRecovery(
|
|
36
|
+
run: StoredRun,
|
|
37
|
+
latest: readonly SessionEvent[],
|
|
38
|
+
): StoppedRunRecoveryPlan {
|
|
39
|
+
requireStoredRunV1(run);
|
|
40
|
+
if (!run.stopRequestedAt) {
|
|
41
|
+
throw new Error(`run "${run.runId}" has no durable stop intent`);
|
|
42
|
+
}
|
|
43
|
+
const admissionFor = (kind: "model" | "tool", effectId: string) => {
|
|
44
|
+
const admission = run.effectAdmissions.find(
|
|
45
|
+
(candidate) => candidate.effectId === effectId,
|
|
46
|
+
);
|
|
47
|
+
if (admission && admission.kind !== kind) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`effect admission "${effectId}" collides with ${admission.kind}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return admission?.outcome;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const model = latestModelRequestJournalState(run.events);
|
|
56
|
+
const tools = validateToolOccurrenceJournal(run.events);
|
|
57
|
+
const openTools = [...tools.values()].filter(
|
|
58
|
+
(entry) => entry.intent !== undefined && entry.result === undefined,
|
|
59
|
+
);
|
|
60
|
+
if (
|
|
61
|
+
(model.status === "unresolved" &&
|
|
62
|
+
admissionFor("model", model.request.request.requestId) === "admitted") ||
|
|
63
|
+
openTools.some(
|
|
64
|
+
(entry) =>
|
|
65
|
+
admissionFor("tool", entry.occurrence.occurrenceId) === "admitted",
|
|
66
|
+
)
|
|
67
|
+
) {
|
|
68
|
+
return { kind: "reconcile" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const session = new Session(run.sessionId, () => {}, latest);
|
|
72
|
+
if (model.status === "unresolved") {
|
|
73
|
+
session.append({
|
|
74
|
+
type: "model/effect-not-started",
|
|
75
|
+
turn: model.request.turn,
|
|
76
|
+
step: model.request.step,
|
|
77
|
+
requestId: model.request.request.requestId,
|
|
78
|
+
reason: "Durable Stop fenced provider execution before admission",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
for (const entry of openTools) {
|
|
82
|
+
const intent = entry.intent!;
|
|
83
|
+
session.append({
|
|
84
|
+
type: "tool/result",
|
|
85
|
+
turn: intent.turn,
|
|
86
|
+
step: intent.step,
|
|
87
|
+
occurrenceId: intent.occurrenceId,
|
|
88
|
+
name: intent.name,
|
|
89
|
+
content: "Durable Stop fenced tool execution before admission.",
|
|
90
|
+
isError: true,
|
|
91
|
+
status: "interrupted",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
session.reconcileInterrupted();
|
|
95
|
+
return {
|
|
96
|
+
kind: "cancel",
|
|
97
|
+
events: [...session.events.slice(run.previousEventCount)],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function planBotRunRecovery(
|
|
102
|
+
run: StoredRun,
|
|
103
|
+
latest: readonly SessionEvent[],
|
|
104
|
+
): BotRunRecoveryPlan {
|
|
105
|
+
return planKernelBotRunRecovery(run, latest, storedRunCodecV1);
|
|
106
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// The Bot Durable Object's half of the Routines seam.
|
|
2
|
+
//
|
|
3
|
+
// "The Bot's Durable Object is the authority for everything Bot-scoped: …
|
|
4
|
+
// durable scheduling, Routines, Assignments." The Routines Package holds the
|
|
5
|
+
// records, the codecs, the command semantics and the scheduler; this module
|
|
6
|
+
// supplies the two things the Package cannot own — the Durable Object's storage,
|
|
7
|
+
// and the one call that admits a Turn.
|
|
8
|
+
//
|
|
9
|
+
// FIRING IS AN IN-OBJECT CALL. `authority.run` is a method on the kernel
|
|
10
|
+
// authority, reached from `settleScheduledWork` inside the object. No HTTP path
|
|
11
|
+
// and no RPC reaches it, so nothing outside the Bot can cause a Routine to run
|
|
12
|
+
// as an automation Turn.
|
|
13
|
+
//
|
|
14
|
+
// HIBERNATION. Nothing here reaches a Computer. "The Agent loop, Memory,
|
|
15
|
+
// Skills, Package composition, and Routines function correctly while the
|
|
16
|
+
// Computer is hibernated and do not wake it": a Routine is Durable Object
|
|
17
|
+
// storage, an alarm, and a Turn.
|
|
18
|
+
import {
|
|
19
|
+
RoutineScheduler,
|
|
20
|
+
type RoutineFireOutcomeV1,
|
|
21
|
+
} from "@frockbot/plugin-routines/scheduler";
|
|
22
|
+
import {
|
|
23
|
+
routineSessionIdV1,
|
|
24
|
+
type RoutineFireV1,
|
|
25
|
+
} from "@frockbot/plugin-routines/firing";
|
|
26
|
+
import {
|
|
27
|
+
RoutineStore,
|
|
28
|
+
type RoutineHookMinterV1,
|
|
29
|
+
type RoutineStorageV1,
|
|
30
|
+
} from "@frockbot/plugin-routines/store";
|
|
31
|
+
import {
|
|
32
|
+
mintRoutineHookTokenV1,
|
|
33
|
+
routineHookDigestV1,
|
|
34
|
+
} from "@frockbot/plugin-routines/hook";
|
|
35
|
+
import { routineHookPathV1 } from "@frockbot/plugin-routines/shared";
|
|
36
|
+
import type { RoutinesRuntimeHostV1 } from "@frockbot/plugin-routines/agent";
|
|
37
|
+
import { routineHandoffTextV1 } from "@frockbot/plugin-routines/inbox";
|
|
38
|
+
import {
|
|
39
|
+
routineTerminalRecordsV1,
|
|
40
|
+
type RoutineTerminalRecordsV1,
|
|
41
|
+
} from "@frockbot/plugin-routines/inbox-store";
|
|
42
|
+
import { decodeRoutineRecordV1 } from "@frockbot/plugin-routines/records";
|
|
43
|
+
import { routineKeyV1 } from "@frockbot/plugin-routines/storage-keys";
|
|
44
|
+
import {
|
|
45
|
+
ROUTINE_RUN_EVENT_MAX,
|
|
46
|
+
type RoutineInboxEntryViewV1,
|
|
47
|
+
type RoutineRunDetailViewV1,
|
|
48
|
+
} from "@frockbot/plugin-routines/shared";
|
|
49
|
+
import type { RoutineInboxEntryV1 } from "@frockbot/plugin-routines/inbox";
|
|
50
|
+
import type { SessionEvent } from "@frockbot/kernel-contracts";
|
|
51
|
+
|
|
52
|
+
/** The Bot and User whose Routines a caller may reach. */
|
|
53
|
+
export interface BotRoutinesIdentity {
|
|
54
|
+
userId: string;
|
|
55
|
+
botId: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The run, Turn, and Session a Bot-authored Routine records as its writer. */
|
|
59
|
+
export interface BotRoutinesTurn {
|
|
60
|
+
runId: string;
|
|
61
|
+
turnId: string;
|
|
62
|
+
sessionId: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The Routines authority for one Bot Durable Object: the record store and the
|
|
67
|
+
* scheduler that fires it, built together because the command path needs the
|
|
68
|
+
* scheduler (`routine/run`) and the scheduler needs the records.
|
|
69
|
+
*
|
|
70
|
+
* `DurableObjectState.storage` already satisfies `RoutineStorageV1`; naming the
|
|
71
|
+
* narrow seam here is what keeps the Package testable without a Durable Object.
|
|
72
|
+
*/
|
|
73
|
+
export function createBotRoutines(
|
|
74
|
+
storage: RoutineStorageV1,
|
|
75
|
+
hookKeys?: RoutineHookMinterV1,
|
|
76
|
+
): {
|
|
77
|
+
store: RoutineStore;
|
|
78
|
+
scheduler: RoutineScheduler;
|
|
79
|
+
} {
|
|
80
|
+
const scheduler = new RoutineScheduler(storage);
|
|
81
|
+
return {
|
|
82
|
+
scheduler,
|
|
83
|
+
store: new RoutineStore(storage, {
|
|
84
|
+
firings: scheduler,
|
|
85
|
+
...(hookKeys ? { hookKeys } : {}),
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The webhook key minter for one Bot.
|
|
92
|
+
*
|
|
93
|
+
* The token is derived from the Worker secret and the Routine's identity, so it
|
|
94
|
+
* is reproducible and never stored; what the Bot keeps is its digest. Without
|
|
95
|
+
* the secret there is no minter at all, and a webhook Routine is refused with
|
|
96
|
+
* that reason rather than given a key that cannot be verified.
|
|
97
|
+
*/
|
|
98
|
+
export function createBotRoutineHookMinter(
|
|
99
|
+
identity: () => Promise<BotRoutinesIdentity | undefined>,
|
|
100
|
+
secret: string | undefined,
|
|
101
|
+
): RoutineHookMinterV1 | undefined {
|
|
102
|
+
if (!secret) return undefined;
|
|
103
|
+
return {
|
|
104
|
+
async mint({ routineId, keyVersion }) {
|
|
105
|
+
// The Bot's durable identity, not a constructor argument: a Durable
|
|
106
|
+
// Object learns who it is from its own storage, and a key that named the
|
|
107
|
+
// wrong Bot would verify at the edge against an object that never holds it.
|
|
108
|
+
const owner = await identity();
|
|
109
|
+
if (!owner) {
|
|
110
|
+
throw new Error("this Bot has no durable identity to key a webhook to");
|
|
111
|
+
}
|
|
112
|
+
const token = await mintRoutineHookTokenV1(secret, {
|
|
113
|
+
u: owner.userId,
|
|
114
|
+
b: owner.botId,
|
|
115
|
+
r: routineId,
|
|
116
|
+
v: keyVersion,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
token,
|
|
120
|
+
digest: await routineHookDigestV1(token),
|
|
121
|
+
path: routineHookPathV1(owner.botId, routineId),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Kept for callers that only want the record store. */
|
|
128
|
+
export function createBotRoutineStore(storage: RoutineStorageV1): RoutineStore {
|
|
129
|
+
return createBotRoutines(storage).store;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The Turn command one firing is admitted as.
|
|
134
|
+
*
|
|
135
|
+
* `turnType: "automation"` is the ceiling the firing runs under, and
|
|
136
|
+
* `origin` names the Routine and the firing, so the run stays attributable
|
|
137
|
+
* after the bounded run log has trimmed its index row away. The Session is the
|
|
138
|
+
* Routine's own — never the User's visible conversation.
|
|
139
|
+
*/
|
|
140
|
+
export function routineTurnCommandV1(
|
|
141
|
+
identity: BotRoutinesIdentity,
|
|
142
|
+
fire: RoutineFireV1,
|
|
143
|
+
acceptedAt: string,
|
|
144
|
+
) {
|
|
145
|
+
return {
|
|
146
|
+
userId: identity.userId,
|
|
147
|
+
botId: identity.botId,
|
|
148
|
+
runId: fire.fireId,
|
|
149
|
+
sessionId: routineSessionIdV1(fire.routineId),
|
|
150
|
+
acceptedAt,
|
|
151
|
+
text: fire.cue,
|
|
152
|
+
turnType: "automation" as const,
|
|
153
|
+
origin: {
|
|
154
|
+
kind: "routine" as const,
|
|
155
|
+
routineId: fire.routineId,
|
|
156
|
+
fireId: fire.fireId,
|
|
157
|
+
trigger: fire.trigger,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* What the run log records for a firing, read off the durable run rather than
|
|
164
|
+
* off the completion value: the run record is the authority for whether the
|
|
165
|
+
* Turn succeeded, and it survives an eviction that loses the value.
|
|
166
|
+
*/
|
|
167
|
+
export function routineFireOutcomeV1(
|
|
168
|
+
run: { status: string; failure?: string; responseText?: string } | undefined,
|
|
169
|
+
thrown?: unknown,
|
|
170
|
+
): RoutineFireOutcomeV1 {
|
|
171
|
+
if (thrown !== undefined) {
|
|
172
|
+
return {
|
|
173
|
+
status: "failed",
|
|
174
|
+
summary: thrown instanceof Error ? thrown.message : String(thrown),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (!run) return { status: "failed", summary: "the firing recorded no run" };
|
|
178
|
+
if (run.status === "cancelled") {
|
|
179
|
+
return {
|
|
180
|
+
status: "cancelled",
|
|
181
|
+
...(run.failure === undefined ? {} : { summary: run.failure }),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (run.status === "completed") {
|
|
185
|
+
// A Turn that ended by handing off writes no assistant message, so its
|
|
186
|
+
// response text is empty; the log records the outcome and leaves the
|
|
187
|
+
// summary off rather than carrying an empty one.
|
|
188
|
+
return {
|
|
189
|
+
status: "ok",
|
|
190
|
+
...(run.responseText === undefined || run.responseText.trim().length === 0
|
|
191
|
+
? {}
|
|
192
|
+
: { summary: run.responseText }),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
status: "failed",
|
|
197
|
+
summary: run.failure ?? `the firing's run is ${run.status}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The Routines seam one admitted Turn runs under. A Turn is required: a Bot
|
|
203
|
+
* writes a Routine only inside a Turn whose Session and Turn its provenance can
|
|
204
|
+
* name, exactly as it writes a Skill or authors a Package.
|
|
205
|
+
*/
|
|
206
|
+
export function createBotRoutinesHost(
|
|
207
|
+
identity: BotRoutinesIdentity,
|
|
208
|
+
turn: BotRoutinesTurn,
|
|
209
|
+
store: RoutineStore,
|
|
210
|
+
): RoutinesRuntimeHostV1 {
|
|
211
|
+
return {
|
|
212
|
+
botId: identity.botId,
|
|
213
|
+
writer: {
|
|
214
|
+
sessionId: turn.sessionId,
|
|
215
|
+
turnId: turn.turnId,
|
|
216
|
+
runId: turn.runId,
|
|
217
|
+
},
|
|
218
|
+
list: () => store.list(identity.botId),
|
|
219
|
+
execute: (command, writer) => store.execute(command, writer),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The Routine a settled run belongs to, or `undefined` when it is an ordinary
|
|
225
|
+
* conversational Turn. Read off the durable admission record, so it survives an
|
|
226
|
+
* eviction and a trimmed run log alike.
|
|
227
|
+
*/
|
|
228
|
+
export function settledRoutineOriginV1(run: {
|
|
229
|
+
admission?: {
|
|
230
|
+
turnType?: string;
|
|
231
|
+
origin?: { kind: string; routineId?: string };
|
|
232
|
+
};
|
|
233
|
+
}): { routineId: string } | undefined {
|
|
234
|
+
if (run.admission?.turnType !== "automation") return undefined;
|
|
235
|
+
const origin = run.admission.origin;
|
|
236
|
+
// A `subagent` origin reaches here on no path today — it is not an
|
|
237
|
+
// `automation` Turn — but the kind is checked rather than assumed, and the id
|
|
238
|
+
// it carries is checked with it.
|
|
239
|
+
if (!origin || origin.kind !== "routine" || !origin.routineId) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
return { routineId: origin.routineId };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The records one settled automation Turn contributes to the transaction that
|
|
247
|
+
* settles it: its completion-inbox entry, and — only when the Turn called
|
|
248
|
+
* `wake_parent` — the pending input the Bot's next conversational Turn is owed.
|
|
249
|
+
*
|
|
250
|
+
* A failed or cancelled firing reaches none of this: `completeStoredRun` is the
|
|
251
|
+
* only caller, so a firing that did not complete leaves a `failed` run-log
|
|
252
|
+
* entry and no inbox entry, which is the row's "durable, visible failure".
|
|
253
|
+
*/
|
|
254
|
+
export async function routineTerminalRecordsForRunV1(input: {
|
|
255
|
+
run: {
|
|
256
|
+
runId: string;
|
|
257
|
+
events: readonly { type: string }[];
|
|
258
|
+
responseText?: string;
|
|
259
|
+
admission?: {
|
|
260
|
+
turnType?: string;
|
|
261
|
+
origin?: { kind: string; routineId?: string };
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
read<T>(key: string): Promise<T | undefined>;
|
|
265
|
+
now: string;
|
|
266
|
+
}): Promise<RoutineTerminalRecordsV1 | undefined> {
|
|
267
|
+
const origin = settledRoutineOriginV1(input.run);
|
|
268
|
+
if (!origin) return undefined;
|
|
269
|
+
const stored = await input.read<unknown>(routineKeyV1(origin.routineId));
|
|
270
|
+
const name =
|
|
271
|
+
stored === undefined
|
|
272
|
+
? origin.routineId
|
|
273
|
+
: decodeRoutineRecordV1(stored).name;
|
|
274
|
+
const handoff = routineHandoffTextV1(input.run.events);
|
|
275
|
+
return routineTerminalRecordsV1({
|
|
276
|
+
runId: input.run.runId,
|
|
277
|
+
routineId: origin.routineId,
|
|
278
|
+
routineName: name,
|
|
279
|
+
now: input.now,
|
|
280
|
+
read: input.read,
|
|
281
|
+
...(handoff === undefined ? {} : { handoff }),
|
|
282
|
+
...(input.run.responseText === undefined
|
|
283
|
+
? {}
|
|
284
|
+
: { responseText: input.run.responseText }),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** One inbox entry, as the hosted client is told it. Never the wake id. */
|
|
289
|
+
export function routineInboxEntryViewV1(
|
|
290
|
+
entry: RoutineInboxEntryV1,
|
|
291
|
+
): RoutineInboxEntryViewV1 {
|
|
292
|
+
return {
|
|
293
|
+
schemaVersion: 1,
|
|
294
|
+
entryId: entry.entryId,
|
|
295
|
+
runId: entry.runId,
|
|
296
|
+
routineId: entry.routineId,
|
|
297
|
+
text: entry.text,
|
|
298
|
+
attribution: entry.attribution,
|
|
299
|
+
createdAt: entry.createdAt,
|
|
300
|
+
acknowledged: entry.acknowledged,
|
|
301
|
+
...(entry.acknowledgedAt === undefined
|
|
302
|
+
? {}
|
|
303
|
+
: { acknowledgedAt: entry.acknowledgedAt }),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** What one event of an automation run says, in one line, for the run log. */
|
|
308
|
+
function routineRunEventSummaryV1(event: SessionEvent): string | undefined {
|
|
309
|
+
switch (event.type) {
|
|
310
|
+
case "turn/start":
|
|
311
|
+
return "The firing's Turn started.";
|
|
312
|
+
case "user/message":
|
|
313
|
+
return `Cue: ${event.text}`;
|
|
314
|
+
case "assistant/message":
|
|
315
|
+
return event.toolCalls.length === 0
|
|
316
|
+
? `Assistant: ${event.text}`
|
|
317
|
+
: `Assistant called ${event.toolCalls.map((call) => call.name).join(", ")}.`;
|
|
318
|
+
case "tool/result":
|
|
319
|
+
return `${event.name} ${event.isError ? "failed" : "returned"}: ${event.content}`;
|
|
320
|
+
case "wake/parent":
|
|
321
|
+
return `Handed off to the conversation: ${event.message}`;
|
|
322
|
+
case "turn/end":
|
|
323
|
+
return `The firing's Turn ended: ${event.outcome}${
|
|
324
|
+
event.reason === undefined ? "" : ` (${event.reason})`
|
|
325
|
+
}`;
|
|
326
|
+
default:
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* One automation run, read-only.
|
|
333
|
+
*
|
|
334
|
+
* It carries no model request and no tool input: the run log answers "what did
|
|
335
|
+
* this firing do", and the durable log stays the place a full reconstruction
|
|
336
|
+
* comes from.
|
|
337
|
+
*/
|
|
338
|
+
export function routineRunDetailViewV1(
|
|
339
|
+
botId: string,
|
|
340
|
+
routineId: string,
|
|
341
|
+
run: {
|
|
342
|
+
runId: string;
|
|
343
|
+
status: string;
|
|
344
|
+
acceptedAt: string;
|
|
345
|
+
input: string;
|
|
346
|
+
events: readonly SessionEvent[];
|
|
347
|
+
responseText?: string;
|
|
348
|
+
failure?: string;
|
|
349
|
+
},
|
|
350
|
+
): RoutineRunDetailViewV1 {
|
|
351
|
+
const events: RoutineRunDetailViewV1["events"] = [];
|
|
352
|
+
for (const event of run.events) {
|
|
353
|
+
const summary = routineRunEventSummaryV1(event);
|
|
354
|
+
if (summary === undefined) continue;
|
|
355
|
+
events.push({
|
|
356
|
+
type: event.type,
|
|
357
|
+
at: event.timestamp,
|
|
358
|
+
summary: summary.slice(0, 2_000),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const outcome = run.failure ?? run.responseText;
|
|
362
|
+
return {
|
|
363
|
+
schemaVersion: 1,
|
|
364
|
+
botId,
|
|
365
|
+
routineId,
|
|
366
|
+
runId: run.runId,
|
|
367
|
+
status: run.status,
|
|
368
|
+
admittedAt: run.acceptedAt,
|
|
369
|
+
input: run.input.slice(0, 16_000),
|
|
370
|
+
events: events.slice(-ROUTINE_RUN_EVENT_MAX),
|
|
371
|
+
...(outcome === undefined || outcome.length === 0
|
|
372
|
+
? {}
|
|
373
|
+
: { outcome: outcome.slice(0, 4_000) }),
|
|
374
|
+
};
|
|
375
|
+
}
|