@fungi.computer/shiitake-extensions 0.4.0 → 0.5.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.
@@ -0,0 +1,182 @@
1
+ import { bindAgentExtension } from "@fungi.computer/shiitake/client";
2
+ import * as Schema from "effect/Schema";
3
+ import { z } from "zod";
4
+ import { almanacContract, Schedule } from "./almanac/contract.js";
5
+ import { Loop, loopContract } from "./loop/contract.js";
6
+ const identity = z.string().min(1).max(256);
7
+ /** Exclusive native key within one Agent's Almanac-then-Loop traversal. */
8
+ export const agentScheduleCursorSchema = z
9
+ .strictObject({
10
+ /** Agent whose retained schedules this continuation traverses. */
11
+ agentId: identity,
12
+ /** Native owner to continue; Almanac precedes Loop in a fresh traversal. */
13
+ kind: z.union([z.literal("almanac"), z.literal("loop")]),
14
+ /** Exclusive Almanac schedule ID or Loop Session ID; absent starts the kind. */
15
+ after: identity.optional(),
16
+ })
17
+ .refine(({ kind, after }) => Schema.is(kind === "almanac"
18
+ ? almanacContract.commands.list.input
19
+ : loopContract.commands.list.input)(after === undefined ? {} : { after }), { message: "Invalid schedule continuation key" });
20
+ /** One bounded read; a full native page conservatively carries continuation. */
21
+ export const agentScheduleListInputSchema = z.strictObject({
22
+ /** Native continuation from the previous page of this Agent's schedules. */
23
+ after: agentScheduleCursorSchema.optional(),
24
+ /** Shared row budget across both native kinds; defaults to and cannot exceed 64. */
25
+ limit: z.number().int().min(1).max(64).default(64),
26
+ });
27
+ /** Content-bearing read projection, decoded by the retained native contracts. */
28
+ export const agentScheduleRowSchema = Schema.Union([
29
+ Schema.Struct({
30
+ /** Agent that owns this Almanac schedule. */
31
+ agentId: Schema.String,
32
+ /** Identifies the native Almanac schedule representation. */
33
+ kind: Schema.Literal("almanac"),
34
+ /** Retained Almanac row, including its instruction and lifecycle state. */
35
+ schedule: Schedule,
36
+ }),
37
+ Schema.Struct({
38
+ /** Agent that owns this Session's Loop. */
39
+ agentId: Schema.String,
40
+ /** Identifies the native Loop representation. */
41
+ kind: Schema.Literal("loop"),
42
+ /** Retained Loop row with the Session identity used to address its owner. */
43
+ schedule: Schema.Struct({
44
+ ...Loop.fields,
45
+ /** Session containing this exact Loop. */
46
+ sessionId: loopContract.commands.pauseLoop.input.fields.sessionId,
47
+ }),
48
+ }),
49
+ ]);
50
+ /** Exact listed identity; a Loop target never selects its later replacement. */
51
+ export const agentScheduleTargetSchema = z.discriminatedUnion("kind", [
52
+ z.strictObject({
53
+ /** Selects the native Almanac control. */
54
+ kind: z.literal("almanac"),
55
+ /** Agent whose owner the caller must admit before controlling this target. */
56
+ agentId: identity,
57
+ /** Exact native Almanac schedule to pause or cancel. */
58
+ scheduleId: identity.refine(Schema.is(Schedule.fields.id)),
59
+ }),
60
+ z.strictObject({
61
+ /** Selects an identified native Loop control. */
62
+ kind: z.literal("loop"),
63
+ /** Agent whose owner the caller must admit before controlling this target. */
64
+ agentId: identity,
65
+ /** Session containing the listed Loop. */
66
+ sessionId: identity.refine(Schema.is(loopContract.commands.pauseLoop.input.fields.sessionId)),
67
+ /** Exact listed Loop identity; a replacement must not be controlled instead. */
68
+ loopId: identity.refine(Schema.is(Loop.fields.loopId)),
69
+ }),
70
+ ]);
71
+ /** A bounded content-bearing page; owner failures reject rather than look empty. */
72
+ export const agentSchedulePageSchema = Schema.Struct({
73
+ /** At most 64 retained rows, ordered Almanac first and then Loop. */
74
+ items: Schema.Array(agentScheduleRowSchema).check(Schema.isMaxLength(64)),
75
+ /** Exclusive continuation on a full page, or null when both kinds are exhausted. */
76
+ next: Schema.NullOr(Schema.Struct({
77
+ /** Agent whose traversal the next request must continue. */
78
+ agentId: Schema.String,
79
+ /** Native owner to continue within that Agent. */
80
+ kind: Schema.Literals(["almanac", "loop"]),
81
+ /** Exclusive schedule ID or Session ID; absent starts the native kind. */
82
+ after: Schema.optionalKey(Schema.String),
83
+ })),
84
+ });
85
+ /** Manage-only result: exact target and native state, never prompt or history. */
86
+ export const agentScheduleControlReceiptSchema = Schema.Union([
87
+ Schema.Struct({
88
+ /** Agent whose Almanac owner completed the transition. */
89
+ agentId: Schema.String,
90
+ /** Identifies the controlled native Almanac schedule. */
91
+ kind: Schema.Literal("almanac"),
92
+ /** Identity of the schedule that was paused or cancelled. */
93
+ scheduleId: Schedule.fields.id,
94
+ /** Native lifecycle state returned after the transition. */
95
+ state: Schedule.fields.state,
96
+ }),
97
+ Schema.Struct({
98
+ /** Agent whose Loop owner completed the transition. */
99
+ agentId: Schema.String,
100
+ /** Identifies the controlled native Loop. */
101
+ kind: Schema.Literal("loop"),
102
+ /** Session containing the controlled Loop. */
103
+ sessionId: loopContract.commands.pauseLoop.input.fields.sessionId,
104
+ /** Exact Loop identity checked by the native transition. */
105
+ loopId: Loop.fields.loopId,
106
+ /** Native lifecycle state returned after the transition. */
107
+ state: Loop.fields.state,
108
+ }),
109
+ ]);
110
+ /**
111
+ * Read one already-admitted Agent, without scanning Sessions or draining pages.
112
+ * The caller owns Agent, home and resource authorization and the owner's lifetime.
113
+ */
114
+ export async function readAgentSchedulePage(agentId, owner, input = {}) {
115
+ const { after, limit } = agentScheduleListInputSchema.parse(input);
116
+ if (after !== undefined && after.agentId !== agentId)
117
+ throw new Error("Schedule continuation belongs to another Agent");
118
+ const items = [];
119
+ if (after?.kind !== "loop") {
120
+ const nativeInput = after?.after === undefined ? { limit } : { limit, after: after.after };
121
+ const rows = await bindAgentExtension(owner, almanacContract).call("list", nativeInput);
122
+ for (const schedule of rows)
123
+ items.push({ agentId, kind: "almanac", schedule });
124
+ const last = rows.at(-1);
125
+ if (items.length === limit && last !== undefined)
126
+ return {
127
+ items,
128
+ next: { agentId, kind: "almanac", after: last.id },
129
+ };
130
+ }
131
+ const remaining = limit - items.length;
132
+ const cursor = after?.kind === "loop" ? after.after : undefined;
133
+ const nativeInput = cursor === undefined
134
+ ? { limit: remaining }
135
+ : { limit: remaining, after: cursor };
136
+ const rows = await bindAgentExtension(owner, loopContract).call("list", nativeInput);
137
+ for (const schedule of rows)
138
+ items.push({ agentId, kind: "loop", schedule });
139
+ const last = rows.at(-1);
140
+ return {
141
+ items,
142
+ next: items.length === limit && last !== undefined
143
+ ? { agentId, kind: "loop", after: last.sessionId }
144
+ : null,
145
+ };
146
+ }
147
+ /**
148
+ * Explicit content-returning control for callers admitted to both read and manage.
149
+ * In particular, private Local retains its native-row reply. The supplied owner
150
+ * must already be admitted for target.agentId; this helper grants no authority.
151
+ */
152
+ export async function controlAgentScheduleWithContent(owner, input, action) {
153
+ const target = agentScheduleTargetSchema.parse(input);
154
+ if (target.kind === "almanac") {
155
+ const schedule = await bindAgentExtension(owner, almanacContract).call(action === "pause" ? "pause" : "cancel", { scheduleId: target.scheduleId });
156
+ return { agentId: target.agentId, kind: "almanac", schedule };
157
+ }
158
+ const loop = await bindAgentExtension(owner, loopContract).call(action === "pause" ? "pauseLoop" : "stopLoop", { sessionId: target.sessionId, loopId: target.loopId });
159
+ return {
160
+ agentId: target.agentId,
161
+ kind: "loop",
162
+ schedule: { ...loop, sessionId: target.sessionId },
163
+ };
164
+ }
165
+ /** Control an already-admitted owner; manage-only callers receive no read content. */
166
+ export async function controlAgentSchedule(owner, target, action) {
167
+ const row = await controlAgentScheduleWithContent(owner, target, action);
168
+ return row.kind === "almanac"
169
+ ? {
170
+ agentId: row.agentId,
171
+ kind: "almanac",
172
+ scheduleId: row.schedule.id,
173
+ state: row.schedule.state,
174
+ }
175
+ : {
176
+ agentId: row.agentId,
177
+ kind: "loop",
178
+ sessionId: row.schedule.sessionId,
179
+ loopId: row.schedule.loopId,
180
+ state: row.schedule.state,
181
+ };
182
+ }
@@ -28,6 +28,7 @@
28
28
  | [question/host](question/host.md) | - |
29
29
  | [roster](roster.md) | - |
30
30
  | [roster/conversation-agents](roster/conversation-agents.md) | - |
31
+ | [schedules](schedules.md) | - |
31
32
  | [skills/host](skills/host.md) | - |
32
33
  | [skills/source](skills/source.md) | - |
33
34
  | [subagents/client](subagents/client.md) | - |
@@ -244,7 +244,7 @@ Session-owned goal controls and their visible projection.
244
244
  | `commands.clear.output` | `Struct`\<\{ \}\> | - | A successful clear has no result fields. |
245
245
  | `commands.set` | `object` | - | Set an objective against the revision the person saw. |
246
246
  | `commands.set.scope` | `"session"` | `"session"` | The Session owns this goal. |
247
- | `commands.set.input` | `Struct`\<\{ `objective`: `String`; `expectedRevision`: `NullOr`\<`String`\>; `requestId`: `String`; \}\> | - | Decode the new objective and replacement fence. |
247
+ | `commands.set.input` | `Struct`\<\{ `objective`: `String`; `expectedRevision`: `NullOr`\<`String`\>; `requestId`: `String`; `adoptCommandId`: `optionalKey`\<`String`\>; \}\> | - | Decode the new objective and replacement fence. |
248
248
  | `commands.set.output` | `Struct`\<\{ `goal`: `Struct`\<\{ `revision`: `String`; `objective`: `String`; `state`: `Literals`\<readonly \[`"active"`, `"paused"`\]\>; `startedAt`: `Int`; `pauseCause`: `NullOr`\<`Struct`\<\{ `kind`: `Literals`\<...\>; `detail`: `NullOr`\<...\>; \}\>\>; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | - | Return the goal and its admission receipt. |
249
249
  | <a id="property-projection"></a> `projection` | `object` | - | Current and ended goals visible to clients. |
250
250
  | `projection.schema` | `Struct`\<\{ `goal`: `NullOr`\<`Struct`\<\{ `revision`: `String`; `objective`: `String`; `state`: `Literals`\<readonly \[`"active"`, `"paused"`\]\>; `startedAt`: `Int`; `pauseCause`: `NullOr`\<`Struct`\<\{ `kind`: `Literals`\<...\>; `detail`: `NullOr`\<...\>; \}\>\>; \}\>\>; `ended`: `$Array`\<`Struct`\<\{ `sequence`: `Int`; `objective`: `String`; `outcome`: `Literals`\<readonly \[`"achieved"`, `"cleared"`\]\>; `reason`: `NullOr`\<`String`\>; `endedAt`: `Int`; `afterSequence`: `Int`; \}\>\>; `omitted`: `Int`; \}\> | `GoalProjection` | Decoder for the goal projection. |
@@ -128,37 +128,49 @@ Loop's contract: Session-scope controls and one projection slot.
128
128
  const loopContract: object;
129
129
  ```
130
130
 
131
- Session-owned loop controls and their visible state.
131
+ Session-owned current-Loop controls and Agent-scoped identified-Loop controls.
132
132
 
133
133
  #### Type Declaration
134
134
 
135
- | Name | Type | Default value | Description |
136
- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ---------------------------------------------------------- |
137
- | <a id="property-id"></a> `id` | `string` & `Brand`\<`"ExtensionId"`\> | - | Stable extension identity. |
138
- | <a id="property-commands"></a> `commands` | `object` | - | Loop operations admitted by the Session owner. |
139
- | `commands.loop` | `object` | - | Open or answer the human loop controls. |
140
- | `commands.loop.scope` | `"session"` | `"session"` | The Session owns this loop. |
141
- | `commands.loop.input` | `Union`\<readonly \[`Struct`\<\{ `action`: `Literal`\<`"start"`\>; `text`: `String`; `requestId`: `String`; \}\>, `Struct`\<\{ `action`: `Literals`\<readonly \[`"status"`, `"pause"`, `"resume"`, `"stop"`, `"stop-ask"`, `"new"`\]\>; `confirmed`: `optional`\<`Boolean`\>; \}\>, `Struct`\<\{ \}\>\]\> | `LoopCommandInput` | Decode the selected human action. |
142
- | `commands.loop.output` | `declare`\<`WhistleCatalogStepV1`, `WhistleCatalogStepV1`\> | `CatalogStep` | Return the server-owned conversation step. |
143
- | `commands.startSelfPaced` | `object` | - | Start a loop whose next occurrence is chosen by the model. |
144
- | `commands.startSelfPaced.scope` | `"session"` | `"session"` | The Session owns this loop. |
145
- | `commands.startSelfPaced.input` | `Struct`\<\{ `prompt`: `String`; `requestId`: `String`; \}\> | - | Decode the repeated prompt and command identity. |
146
- | `commands.startSelfPaced.output` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | `StartResult` | Return the loop and admission receipt. |
147
- | `commands.startFixedInterval` | `object` | - | Start a loop with a fixed cron cadence. |
148
- | `commands.startFixedInterval.scope` | `"session"` | `"session"` | The Session owns this loop. |
149
- | `commands.startFixedInterval.input` | `Struct`\<\{ `expression`: `String`; `timeZone`: `String`; `prompt`: `String`; `requestId`: `String`; \}\> | - | Decode the prompt, cadence and command identity. |
150
- | `commands.startFixedInterval.output` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | `StartResult` | Return the loop and admission receipt. |
151
- | `commands.pause` | `object` | - | Suspend the current loop. |
152
- | `commands.pause.scope` | `"session"` | `"session"` | The Session owns this loop. |
153
- | `commands.pause.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
154
- | `commands.pause.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the paused loop. |
155
- | `commands.resume` | `object` | - | Resume the current paused loop. |
156
- | `commands.resume.scope` | `"session"` | `"session"` | The Session owns this loop. |
157
- | `commands.resume.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
158
- | `commands.resume.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the resumed loop. |
159
- | `commands.stop` | `object` | - | Stop future occurrences of the current loop. |
160
- | `commands.stop.scope` | `"session"` | `"session"` | The Session owns this loop. |
161
- | `commands.stop.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
162
- | `commands.stop.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the stopped loop. |
163
- | <a id="property-projection"></a> `projection` | `object` | - | Current loop visible to Session clients. |
164
- | `projection.schema` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; \}\> | `LoopProjection` | Decoder for projected loop state. |
135
+ | Name | Type | Default value | Description |
136
+ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------ |
137
+ | <a id="property-id"></a> `id` | `string` & `Brand`\<`"ExtensionId"`\> | - | Stable extension identity. |
138
+ | <a id="property-commands"></a> `commands` | `object` | - | Loop operations admitted by their Agent or Session owner. |
139
+ | `commands.list` | `object` | - | Read an Agent's bounded Loop page, including each destination Session. |
140
+ | `commands.list.scope` | `"agent"` | `"agent"` | The Agent owns the collection of its Sessions' Loops. |
141
+ | `commands.list.input` | `Struct`\<\{ `after`: `optionalKey`\<`String`\>; `limit`: `optionalKey`\<`Int`\>; \}\> | - | Page strictly after a Session identity, in identity order. |
142
+ | `commands.list.output` | `$Array`\<`Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; `sessionId`: `String`; \}\>\> | - | Public Loop values with Session targets, never private scheduling state. |
143
+ | `commands.pauseLoop` | `object` | - | Pause an identified Loop; refuse a stale target after replacement. |
144
+ | `commands.pauseLoop.scope` | `"agent"` | `"agent"` | The Agent owns the identified Loop and its Session. |
145
+ | `commands.pauseLoop.input` | `Struct`\<\{ `sessionId`: `String`; `loopId`: `String`; \}\> | `LoopTarget` | The listed Session and Loop identities must both still match. |
146
+ | `commands.pauseLoop.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the paused Loop without cancelling accepted work. |
147
+ | `commands.stopLoop` | `object` | - | Cancel an identified Loop's future occurrences, retaining accepted work. |
148
+ | `commands.stopLoop.scope` | `"agent"` | `"agent"` | The Agent owns the identified Loop and its Session. |
149
+ | `commands.stopLoop.input` | `Struct`\<\{ `sessionId`: `String`; `loopId`: `String`; \}\> | `LoopTarget` | The listed Session and Loop identities must both still match. |
150
+ | `commands.stopLoop.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the cancelled Loop; its Session and history remain. |
151
+ | `commands.loop` | `object` | - | Open or answer the human loop controls. |
152
+ | `commands.loop.scope` | `"session"` | `"session"` | The Session owns this loop. |
153
+ | `commands.loop.input` | `Union`\<readonly \[`Struct`\<\{ `action`: `Literal`\<`"start"`\>; `text`: `String`; `requestId`: `String`; \}\>, `Struct`\<\{ `action`: `Literals`\<readonly \[`"status"`, `"pause"`, `"resume"`, `"stop"`, `"stop-ask"`, `"new"`\]\>; `confirmed`: `optional`\<`Boolean`\>; \}\>, `Struct`\<\{ \}\>\]\> | `LoopCommandInput` | Decode the selected human action. |
154
+ | `commands.loop.output` | `declare`\<`WhistleCatalogStepV1`, `WhistleCatalogStepV1`\> | `CatalogStep` | Return the server-owned conversation step. |
155
+ | `commands.startSelfPaced` | `object` | - | Start a loop whose next occurrence is chosen by the model. |
156
+ | `commands.startSelfPaced.scope` | `"session"` | `"session"` | The Session owns this loop. |
157
+ | `commands.startSelfPaced.input` | `Struct`\<\{ `prompt`: `String`; `requestId`: `String`; \}\> | - | Decode the repeated prompt and command identity. |
158
+ | `commands.startSelfPaced.output` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | `StartResult` | Return the loop and admission receipt. |
159
+ | `commands.startFixedInterval` | `object` | - | Start a loop with a fixed cron cadence. |
160
+ | `commands.startFixedInterval.scope` | `"session"` | `"session"` | The Session owns this loop. |
161
+ | `commands.startFixedInterval.input` | `Struct`\<\{ `expression`: `String`; `timeZone`: `String`; `prompt`: `String`; `requestId`: `String`; \}\> | - | Decode the prompt, cadence and command identity. |
162
+ | `commands.startFixedInterval.output` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; `accepted`: `Struct`\<\{ `status`: `Literal`\<`"accepted"`\>; `commandId`: `String`; \}\>; \}\> | `StartResult` | Return the loop and admission receipt. |
163
+ | `commands.pause` | `object` | - | Suspend the current loop. |
164
+ | `commands.pause.scope` | `"session"` | `"session"` | The Session owns this loop. |
165
+ | `commands.pause.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
166
+ | `commands.pause.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the paused loop. |
167
+ | `commands.resume` | `object` | - | Resume the current paused loop. |
168
+ | `commands.resume.scope` | `"session"` | `"session"` | The Session owns this loop. |
169
+ | `commands.resume.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
170
+ | `commands.resume.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the resumed loop. |
171
+ | `commands.stop` | `object` | - | Stop future occurrences of the current loop. |
172
+ | `commands.stop.scope` | `"session"` | `"session"` | The Session owns this loop. |
173
+ | `commands.stop.input` | `Struct`\<\{ \}\> | - | No additional arguments are required. |
174
+ | `commands.stop.output` | `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\> | `Loop` | Return the stopped loop. |
175
+ | <a id="property-projection"></a> `projection` | `object` | - | Current loop visible to Session clients. |
176
+ | `projection.schema` | `Struct`\<\{ `loop`: `Struct`\<\{ `loopId`: `String`; `origin`: `Literals`\<readonly \[`"human"`, `"model"`\]\>; `expiresAt`: `NullOr`\<`Int`\>; `prompt`: `String`; `mode`: `Literals`\<readonly \[`"self_paced"`, `"fixed_interval"`\]\>; `cron`: `NullOr`\<`Struct`\<\{ `expression`: `String`; `timeZone`: `String`; \}\>\>; `state`: `Literals`\<readonly \[`"active"`, `"paused"`, `"finished"`, `"failed"`, `"cancelled"`\]\>; `commandId`: `String`; `reason`: `NullOr`\<`String`\>; `nextDueAt`: `NullOr`\<`Int`\>; `lastError`: `NullOr`\<`String`\>; `consecutiveFailures`: `Int`; \}\>; \}\> | `LoopProjection` | Decoder for projected loop state. |
@@ -8,6 +8,24 @@
8
8
 
9
9
  # roster/conversation-agents
10
10
 
11
+ ## Type Aliases
12
+
13
+ ### SendingDirectory
14
+
15
+ ```ts
16
+ type SendingDirectory = Omit<AgentControlDirectory, "open"> & Readonly<{
17
+ open: (agentId, signal) => Promise<Readonly<{
18
+ agent: Readonly<{
19
+ session: (sessionId) => Pick<..., ...>;
20
+ }>;
21
+ release: () => Promise<void>;
22
+ }>>;
23
+ }>;
24
+ ```
25
+
26
+ The directory a conversation sends through; opening a recipient only needs its
27
+ Sessions' extension doors.
28
+
11
29
  ## Functions
12
30
 
13
31
  ### assertOrdinaryAgentCreation()
@@ -54,7 +72,7 @@ child.
54
72
 
55
73
  | Name | Type | Description |
56
74
  | ------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
57
- | `recording()` | (`parentSessionId`, `directory`) => `AgentControlDirectory` | Bind acceptance recording to this conversation's existing directory. One conversation's directory: its accepted creates and sends are recorded. |
75
+ | `recording()` | \<`Directory`\>(`parentSessionId`, `directory`) => `Directory` | Bind acceptance recording to this conversation's existing directory. One conversation's directory: its accepted creates and sends are recorded. |
58
76
  | `control()` | (`directoryFor`) => `CapabilitySource` | Agent-control tools bound to the conversation that acquires them. |
59
77
  | `source()` | (`directory`) => [`SubagentSource`](../subagents/host.md#subagentsource) | The footer rows for one conversation; archived and unknown Agents leave it. |
60
78