@stratusagent/core 0.11.6 → 0.11.8

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/index.d.ts CHANGED
@@ -151,9 +151,11 @@ export interface Message {
151
151
  toolCalls?: ToolCall[];
152
152
  toolResult?: ToolResult;
153
153
  /**
154
- * Images sent with a user message. Present only on `user` messages that
155
- * carried one; a provider that can show the model an image sends these
156
- * alongside the text, and one that cannot names them instead.
154
+ * Images sent with a user message, or returned by a tool call (moved
155
+ * here off `ToolResult.images` as the result is recorded). Present only
156
+ * on `user` and `tool` messages that carried one; a provider that can
157
+ * show the model an image sends these alongside the text, and one that
158
+ * cannot names them instead.
157
159
  */
158
160
  images?: ImageAttachment[];
159
161
  /**
@@ -169,6 +171,44 @@ export interface Message {
169
171
  * — the boundary provenance draws, here at the point text enters.
170
172
  */
171
173
  overheard?: boolean;
174
+ /**
175
+ * Appended by `observe`, so it started no turn. `overheard` alone cannot
176
+ * say so: a turn dispatched with `addressed: false` is overheard too, and
177
+ * is a turn. Present only when true; a message observed before this was
178
+ * recorded reads as a turn, which errs toward attributing nothing to an
179
+ * earlier one (`isLatestTurn`).
180
+ */
181
+ observed?: boolean;
182
+ /**
183
+ * The caller's idempotency key for the dispatch that brought this user
184
+ * message (`RunInput.idempotencyKey`). Written in the same save as the
185
+ * message, so a key is never durable without its message or the other
186
+ * way round; `workItemState` reads it back.
187
+ */
188
+ idempotencyKey?: string;
189
+ /**
190
+ * Why the turn this user message started failed, written by
191
+ * `recordTurnFailure` in the save that fails it. The session's
192
+ * `lastError` is the latest turn's and the next turn replaces it; this
193
+ * stays, so `turnFailureFor` can answer for any turn a key names.
194
+ */
195
+ turnError?: string;
196
+ /**
197
+ * This message's turn was continued once after the process running it
198
+ * died (`AgentRunner.continueTurn`). A host fails a second such turn
199
+ * instead of continuing it again: a turn that takes the process down
200
+ * with it would otherwise do so on every start. Present only when true.
201
+ */
202
+ continuedAfterCrash?: boolean;
203
+ /**
204
+ * The turn this message started could reach a provider that runs its own
205
+ * tool loop (a harness): what that turn did is not all in this
206
+ * transcript, so a crash mid-turn cannot be continued from it. Recorded
207
+ * at accept time, because the runtime a crashed turn ran on is not the
208
+ * one a restart resolves if the configuration changed in between.
209
+ * Present only when true; see `RunInput.hostedLoop`.
210
+ */
211
+ hostedLoop?: boolean;
172
212
  }
173
213
  export interface AgentDescriptor {
174
214
  id: string;
@@ -368,7 +408,48 @@ export interface ToolResult {
368
408
  * already hold hostile page content. A new result always carries one.
369
409
  */
370
410
  trust?: TrustLevel;
411
+ /**
412
+ * Images the call handed back through `ExecutionContext.attachImage` — a
413
+ * screenshot, an image file read from disk. In flight only: the runner
414
+ * moves them onto the tool message's `images` as it records the result,
415
+ * so they are stored, budgeted, and trimmed exactly as a user message's
416
+ * images are, and never serialized into the message content or carried
417
+ * on a bus event.
418
+ */
419
+ images?: ImageAttachment[];
371
420
  }
421
+ /**
422
+ * What the model is told about a result labelled `external`, said once
423
+ * here rather than left for each tool to word — a third-party plugin or an
424
+ * MCP server never has to know the convention for its output to carry it.
425
+ */
426
+ export declare const UNTRUSTED_TOOL_RESULT_NOTE: string;
427
+ /**
428
+ * The text a provider hands the model for a tool result — the one
429
+ * serialization every provider and harness bridge uses, so the trust label
430
+ * reaches the model the same way whichever one carries the turn.
431
+ *
432
+ * An `external` result is wrapped in `{ untrusted, untrustedNote, output }`,
433
+ * the vocabulary `web.search` already used in-band. This marks the boundary
434
+ * and nothing more: a model can still follow text inside the envelope, and
435
+ * it is not a prompt-injection defence. Only `external` is wrapped, not
436
+ * `unknown`: `shell.run` declares `unknown`, and a marker on every command's
437
+ * output would teach the model that the marker is noise. A failed result is
438
+ * wrapped too, because the executor labels failures — a fetched server's
439
+ * error text is that server's text.
440
+ *
441
+ * The envelope is applied whatever the output already says, including
442
+ * `web.search`'s own `untrusted: true`. Skipping on a field of the output
443
+ * would let the output decide whether it is marked, and the output is the
444
+ * party not trusted; the repetition costs a sentence.
445
+ *
446
+ * Never mutates the result: the transcript, the dashboard, and the control
447
+ * API read the raw output. Deterministic for a given result, so a replayed
448
+ * transcript is byte-identical and the prompt cache holds. `Tool failed`
449
+ * stays unpunctuated because the API providers replay it from persisted
450
+ * transcripts, where a changed byte is a cache miss.
451
+ */
452
+ export declare const renderToolResultContent: (result: Pick<ToolResult, "ok" | "output" | "error" | "trust">) => string;
372
453
  export type ProviderPart = {
373
454
  type: 'text';
374
455
  text: string;
@@ -379,7 +460,8 @@ export type ProviderPart = {
379
460
  /**
380
461
  * A streamed fragment of an in-progress provider response. Text deltas carry
381
462
  * output as it is generated; tool-call deltas announce that the model has
382
- * started emitting a call (input may still be incomplete). A reset delta
463
+ * started emitting a call (input may still be incomplete); thinking and
464
+ * progress deltas carry nothing but the fact of activity. A reset delta
383
465
  * tells consumers to DISCARD every fragment streamed so far for this
384
466
  * response — emitted when a provider abandons a partial attempt (e.g. a
385
467
  * fallback wrapper retrying after the primary failed mid-stream), so
@@ -400,6 +482,16 @@ export type ProviderDelta = {
400
482
  */
401
483
  | {
402
484
  type: 'thinking';
485
+ }
486
+ /**
487
+ * The provider is working on this response and has nothing to show for
488
+ * it yet: a retry it is waiting out, a context compaction, a message
489
+ * boundary. Content-free like `thinking`, and for the same reader — an
490
+ * activity watchdog that would otherwise take a busy provider for a
491
+ * stalled one. A renderer has nothing to draw for it.
492
+ */
493
+ | {
494
+ type: 'progress';
403
495
  } | {
404
496
  type: 'reset';
405
497
  /**
@@ -1391,6 +1483,13 @@ export declare class InMemoryAgentMemoryStore implements AgentMemoryStore {
1391
1483
  * Resolves named credentials for an agent. Implementations must enforce the
1392
1484
  * agent's `credentials` allowlist so secrets stay scoped per agent.
1393
1485
  */
1486
+ /** Whose a named credential is: one agent's own, or the whole fleet's. */
1487
+ export type CredentialScope = 'agent' | 'shared';
1488
+ /**
1489
+ * How a credential request reaches a person: a form in the conversation's
1490
+ * own channel, or a one-time link the agent passes on.
1491
+ */
1492
+ export type CredentialDelivery = 'form' | 'link';
1394
1493
  export interface CredentialResolver {
1395
1494
  resolve(agent: AgentDefinition, name: string): Promise<string | undefined>;
1396
1495
  }
@@ -1499,6 +1598,99 @@ export declare const uncachedInputTokens: (total: number, ...cached: Array<numbe
1499
1598
  * nothing at all.
1500
1599
  */
1501
1600
  export declare const totalTokenUsage: (records: readonly TokenUsage[]) => TokenUsage | undefined;
1601
+ /**
1602
+ * Facts about how an agent is run that only the host knows, told to the
1603
+ * agent so it does not have to guess them.
1604
+ *
1605
+ * It does guess otherwise, and guesses wrong: an agent asked to reread its
1606
+ * soul after an edit answered that it had none, because there was no
1607
+ * `SOUL.md` in its workspace, which is where other agent runtimes keep one.
1608
+ * Handed in by the host rather than written into the soul because the
1609
+ * paths are the host's to own, and a copy in every soul would go stale the
1610
+ * next time the layout moved, the way the workspace already did once.
1611
+ */
1612
+ export interface AgentRuntimeContext {
1613
+ /** Absolute path of the soul file this agent's persona was loaded from. */
1614
+ soulPath?: string;
1615
+ /**
1616
+ * True when the host reads the soul again before every turn, so an edit
1617
+ * reaches the agent's next reply. The daemon does; a one-shot run reads
1618
+ * it once, and leaving this out says nothing either way.
1619
+ */
1620
+ soulReloads?: boolean;
1621
+ /** Where this agent's files go: `AgentWorkspaces.forAgent`. */
1622
+ workspace?: string;
1623
+ /**
1624
+ * The language the agent writes in, as a tag `isLanguageTag` accepts,
1625
+ * already resolved by the host: the soul's `language:`, then the config
1626
+ * file's. Absent means {@link DEFAULT_LANGUAGE}. Rendered in the reply
1627
+ * section rather than here, so a host that says nothing else still gets
1628
+ * the default rule.
1629
+ */
1630
+ language?: string;
1631
+ /**
1632
+ * What the host has configured to answer as this agent, and what it has
1633
+ * observed: `onFallback` is true once this conversation has switched to
1634
+ * the fallback, which is sticky, so a model asked what it is can tell a
1635
+ * configured default from the one actually answering. The fallback
1636
+ * wrapper sets it on the request it hands the fallback mid-turn, so the
1637
+ * model that took over is not told it is the default.
1638
+ */
1639
+ model?: AgentModelContext;
1640
+ /**
1641
+ * Who can read this conversation, as the channel that carried the turn
1642
+ * reported it on the turn's metadata (`conversationContextFrom`). Per
1643
+ * turn rather than read off the session, so a thread that started before
1644
+ * this existed, or a channel that grew since, is described as it is now.
1645
+ */
1646
+ conversation?: ConversationContext;
1647
+ }
1648
+ /**
1649
+ * What kind of room a conversation is: one person, a few, a closed
1650
+ * channel, or one the whole workspace can read. An agent that did not
1651
+ * know told a person in a Slack DM they were "talking on the terminal",
1652
+ * and would have posted the same reply to a thousand-person channel.
1653
+ */
1654
+ export type ConversationKind = 'direct' | 'group' | 'private' | 'public';
1655
+ export interface ConversationContext {
1656
+ kind: ConversationKind;
1657
+ /** Members as the channel counts them, the agent included. */
1658
+ members?: number;
1659
+ /**
1660
+ * Who a direct message is with. It reaches the system prompt, so a
1661
+ * channel sets it only for someone the operator vouched for (a Slack
1662
+ * principal), never from a name anyone could have typed.
1663
+ */
1664
+ with?: string;
1665
+ /** The turn is a reply in a thread rather than the channel itself. */
1666
+ thread?: boolean;
1667
+ /**
1668
+ * People outside this workspace read it too: a Slack Connect channel,
1669
+ * or one shared across an organization's workspaces. Never set for a
1670
+ * direct message.
1671
+ */
1672
+ shared?: boolean;
1673
+ }
1674
+ /**
1675
+ * The metadata key a channel adapter records a turn's room under: a
1676
+ * `ConversationContext`-shaped object, read back by
1677
+ * `conversationContextFrom`.
1678
+ */
1679
+ export declare const CONVERSATION_METADATA_KEY = "conversation";
1680
+ /**
1681
+ * The room a turn's metadata describes, keeping only what is safe to put
1682
+ * in front of the model, or undefined when it describes none.
1683
+ */
1684
+ export declare const conversationContextFrom: (metadata: JsonObject | undefined) => ConversationContext | undefined;
1685
+ export interface AgentModelContext {
1686
+ provider: string;
1687
+ model?: string;
1688
+ fallback?: {
1689
+ provider: string;
1690
+ model: string;
1691
+ };
1692
+ onFallback?: boolean;
1693
+ }
1502
1694
  export interface ProviderRequest {
1503
1695
  /**
1504
1696
  * The live session. **Persistable**: a provider that saves it, or hands
@@ -1551,6 +1743,12 @@ export interface ProviderRequest {
1551
1743
  * the model decides a description is relevant.
1552
1744
  */
1553
1745
  skills?: SkillDescriptor[];
1746
+ /**
1747
+ * What the host can say about how this agent is run: where its soul and
1748
+ * workspace are. Rendered as the `runtime` section; see
1749
+ * {@link AgentRuntimeContext}.
1750
+ */
1751
+ runtime?: AgentRuntimeContext;
1554
1752
  /**
1555
1753
  * Streaming sink. Adapters that stream call this per fragment and MUST
1556
1754
  * await the returned promise before the next call (backpressure); the
@@ -1657,7 +1855,29 @@ export interface ExecutionContext {
1657
1855
  * no sink gets whatever `outputTrust` declares.
1658
1856
  */
1659
1857
  markTrust?: (trust: TrustLevel) => void;
1858
+ /**
1859
+ * Hand the model an image with this call's result — a screenshot a tool
1860
+ * took, an image file it read — so the agent can actually look at it
1861
+ * rather than be told a path. Checked as it is attached: a format the
1862
+ * model APIs refuse, an image over `IMAGE_ATTACHMENT_MAX_BYTES` or
1863
+ * `IMAGE_ATTACHMENT_MAX_DIMENSION`, bytes that are not the image they
1864
+ * claim to be, or more than `IMAGE_ATTACHMENTS_MAX_TOTAL_BYTES` in one
1865
+ * call throws, so the tool can say so in its own result. Absent when the
1866
+ * host cannot carry images; a tool then says it has no way to show one.
1867
+ *
1868
+ * The result's trust label covers the image too: a screenshot of a web
1869
+ * page is the page's content.
1870
+ */
1871
+ attachImage?: (image: ImageAttachment) => void;
1660
1872
  }
1873
+ /**
1874
+ * Collects what a tool attaches through `ExecutionContext.attachImage`,
1875
+ * held to the limits that sink documents.
1876
+ */
1877
+ export declare const createImageCollector: () => {
1878
+ attach: (image: ImageAttachment) => void;
1879
+ images: () => ImageAttachment[];
1880
+ };
1661
1881
  export interface Tool {
1662
1882
  name: string;
1663
1883
  description?: string;
@@ -1842,6 +2062,82 @@ export interface PendingApprovalRecord {
1842
2062
  * cannot disagree on which message is "the reply".
1843
2063
  */
1844
2064
  export declare const latestTurnReply: (session: Pick<Session, "messages">) => string | undefined;
2065
+ /**
2066
+ * Whether a session's transcript already ends in the turn's answer: its
2067
+ * last response holds no tool call, so the loop owed the model nothing more
2068
+ * and only the save marking the turn completed is missing. A continuation
2069
+ * then completes it without a provider call.
2070
+ *
2071
+ * Judged on the last response as a whole: a response's parts are saved
2072
+ * together and its tool results after them all, so the trailing run of
2073
+ * assistant messages is that one response. Any call in it — even one
2074
+ * followed by text, which a provider may send in that order — means the
2075
+ * model is owed another turn. Read it before `reconcileInterruptedToolCalls`
2076
+ * splices results in.
2077
+ */
2078
+ export declare const lastResponseIsAnswer: (session: Pick<Session, "messages">) => boolean;
2079
+ /**
2080
+ * The end of a turn that answered: marked completed in one save, then
2081
+ * announced. The runner's loop ends here, and so does a turn whose answer
2082
+ * a process saved and then died before marking it completed — which needs
2083
+ * no provider and no current agent definition to finish, so a host can do
2084
+ * it with only its store and bus (`lastResponseIsAnswer` says when).
2085
+ */
2086
+ export declare const completeAnsweredTurn: (finished: Session, store: SessionStore, bus: EventBus) => Promise<Session>;
2087
+ /**
2088
+ * The longest idempotency key a dispatch accepts. Here rather than in the
2089
+ * gateway that enforces it, because a channel composing keys from ids it
2090
+ * does not bound (an agent's, a platform's) has to fit them to it.
2091
+ */
2092
+ export declare const MAX_IDEMPOTENCY_KEY_LENGTH = 256;
2093
+ /**
2094
+ * The reply of the turn a keyed message started (`Message.idempotencyKey`),
2095
+ * where `latestTurnReply` is the newest turn's. A repeated dispatch resolves
2096
+ * with the session as it stands, which may have moved on since: an adapter
2097
+ * answering a redelivered message reads the reply here, or it would answer
2098
+ * the old message with a newer turn's words. Undefined when no message
2099
+ * carries the key, or its turn produced no text.
2100
+ */
2101
+ export declare const turnReplyFor: (session: Pick<Session, "messages">, idempotencyKey: string) => string | undefined;
2102
+ /**
2103
+ * The files the turn a keyed message started produced (`filePathsOf` over
2104
+ * its tool results), the other half of what `turnReplyFor` reads. A
2105
+ * channel uploads a turn's files as the results arrive, so an adapter
2106
+ * answering a repeat of a finished turn — no events, only the session —
2107
+ * has this and nothing else to find them by.
2108
+ *
2109
+ * A path, not the bytes: the transcript never held them. `producedAt` is
2110
+ * when the result naming it was recorded, so a reader can refuse a path
2111
+ * that has been written since — by now it may hold a later turn's file.
2112
+ */
2113
+ export declare const turnFilesFor: (session: Pick<Session, "messages">, idempotencyKey: string) => Array<{
2114
+ path: string;
2115
+ producedAt: string;
2116
+ }>;
2117
+ /**
2118
+ * Fail a session's in-flight turn: `status` and `lastError`, and the error
2119
+ * on the turn's own user message (`Message.turnError`), where the next
2120
+ * turn cannot overwrite it. The one write every path that fails a turn
2121
+ * makes — the runner's and the gateway's sweeps — so `turnFailureFor`
2122
+ * reads the same record whichever failed it. In flight, the turn's message
2123
+ * is the session's newest user message (`observe` refuses to append).
2124
+ */
2125
+ export declare const recordTurnFailure: (session: Session, error: string) => void;
2126
+ /**
2127
+ * Whether the keyed message started the session's latest turn. A message
2128
+ * `observe` appended since started no turn and is passed over
2129
+ * (`Message.observed`); every other user message, keyed or not, addressed
2130
+ * or not, started one.
2131
+ */
2132
+ export declare const isLatestTurn: (session: Pick<Session, "messages">, idempotencyKey: string) => boolean;
2133
+ /**
2134
+ * Why the turn a keyed message started failed, or undefined if it did not.
2135
+ * Read from the message (`recordTurnFailure`), so a turn the session has
2136
+ * moved on from still answers. A transcript written before turns carried
2137
+ * their own failure has only the session's, which is the keyed turn's
2138
+ * while it is the latest (`isLatestTurn`).
2139
+ */
2140
+ export declare const turnFailureFor: (session: Pick<Session, "messages" | "status" | "lastError">, idempotencyKey: string) => string | undefined;
1845
2141
  /**
1846
2142
  * A user message's text as a prompt should carry it. The one place an
1847
2143
  * overheard message is framed, so the API providers' per-message blocks
@@ -1877,6 +2173,22 @@ export declare const promptTextOf: (message: Pick<Message, "content" | "overhear
1877
2173
  * as the answer rather than as a broken endpoint.
1878
2174
  */
1879
2175
  export declare const isUnaddressedTurn: (session: Pick<Session, "messages">) => boolean;
2176
+ /**
2177
+ * Where the dispatch carrying `idempotencyKey` stands in this session:
2178
+ * `undefined` if no message carries the key, `unfinished` if its message
2179
+ * is the one the session's in-flight turn is on, `finished` otherwise.
2180
+ *
2181
+ * Derived, never recorded separately, which is what makes it exact. The
2182
+ * keyed message is written in the same save that sets the status to
2183
+ * `running` (accepted and started are one write), and the save that moves
2184
+ * the status off `running` or `pending_approval` is the turn's final one,
2185
+ * so `finished` is written in the same transaction as the final save by
2186
+ * construction. While a turn is in flight its message is the session's
2187
+ * newest user message — `observe` refuses to append during one, and the
2188
+ * runner's wrap-up notes never reach the store — the same reading
2189
+ * `isUnaddressedTurn` relies on.
2190
+ */
2191
+ export declare const workItemState: (session: Pick<Session, "messages" | "status">, idempotencyKey: string) => "unfinished" | "finished" | undefined;
1880
2192
  export declare const markPromptDelivered: <T>(error: T) => T;
1881
2193
  export declare const promptWasDelivered: (error: unknown) => boolean;
1882
2194
  export interface PromptTextOptions {
@@ -2069,6 +2381,54 @@ export type StratusEvent = {
2069
2381
  reason: ApprovalResolutionReason;
2070
2382
  /** Who decided, when a person did. Channel-native id (a Slack user). */
2071
2383
  actor?: string;
2384
+ }
2385
+ /**
2386
+ * An agent asked for a named credential it does not hold
2387
+ * (`credential.request`), and it has already been put to someone: a form
2388
+ * in the conversation it asked in (`via: 'form'`), or a one-time link the
2389
+ * agent was handed to pass on (`via: 'link'`). The request is delivered
2390
+ * first and announced after, so this is a record of a question that was
2391
+ * asked, never a request for someone to render one. Nothing about it is
2392
+ * a secret, and nothing that answers it ever travels on the bus. `scope` is whose the key would be: the
2393
+ * agent's own (`agent`, the default) or the fleet's (`shared`).
2394
+ *
2395
+ * `metadata` is the session's, for the same reason an approval request
2396
+ * carries it: it says which conversation to ask in.
2397
+ */
2398
+ | {
2399
+ type: 'credential.requested';
2400
+ sessionId: string;
2401
+ agentId: string;
2402
+ requestId: string;
2403
+ name: string;
2404
+ scope: CredentialScope;
2405
+ /** The agent's own words on what it needs the key for. Untrusted text. */
2406
+ reason?: string;
2407
+ /** How it was put to someone. Absent from hosts before links existed, which only had forms. */
2408
+ via?: CredentialDelivery;
2409
+ metadata?: JsonObject;
2410
+ }
2411
+ /**
2412
+ * A requested credential was stored for the agent that asked, and granted
2413
+ * to it. Names only; the value went to the credential store and nowhere
2414
+ * else.
2415
+ *
2416
+ * Emitted once the key is stored, whether or not the grant then landed:
2417
+ * a key added from a chat is a change on disk, and a shared one is
2418
+ * already usable by every agent granted that name, so it is recorded even
2419
+ * when the soul write failed. `grantError` says it did, and why.
2420
+ */
2421
+ | {
2422
+ type: 'credential.provided';
2423
+ sessionId: string;
2424
+ agentId: string;
2425
+ requestId: string;
2426
+ name: string;
2427
+ scope: CredentialScope;
2428
+ /** Who provided it. Channel-native id (a Slack user). */
2429
+ actor?: string;
2430
+ /** Set when the key was stored but could not be added to the soul. */
2431
+ grantError?: string;
2072
2432
  } | {
2073
2433
  type: 'session.completed';
2074
2434
  sessionId: string;
@@ -2421,6 +2781,29 @@ export interface AgentWorkspaces {
2421
2781
  */
2422
2782
  all(): Promise<readonly string[]>;
2423
2783
  }
2784
+ /**
2785
+ * Where one plugin keeps durable state of its own, answered by the host.
2786
+ *
2787
+ * A seam for the reason {@link AgentWorkspaces} is one: the layout is the
2788
+ * host's to own. A plugin that needs to remember something across restarts
2789
+ * — a channel's read position in a store it does not own, a cursor into a
2790
+ * vendor's event log — would otherwise join a path onto `~/.stratus` itself,
2791
+ * and every plugin doing so is one more copy of the layout to find when it
2792
+ * moves. Per plugin, never per agent: what a plugin remembers is its own,
2793
+ * and it makes whatever structure it needs beneath this.
2794
+ */
2795
+ export interface PluginStateDirectory {
2796
+ /**
2797
+ * The directory, created and with its permissions settled, for a caller
2798
+ * about to write. Private to the daemon's user, because what a plugin
2799
+ * keeps is the daemon's business, and a plugin cannot be expected to know
2800
+ * the mode the rest of the state is held to.
2801
+ *
2802
+ * Asked per write rather than captured at setup: a daemon runs for weeks,
2803
+ * and the directory can be removed or replaced with a link under it.
2804
+ */
2805
+ prepare(): string;
2806
+ }
2424
2807
  export interface MemoryRegistrationHandle {
2425
2808
  register(contribution: MemoryStoreContribution): void;
2426
2809
  }
@@ -2471,6 +2854,15 @@ export interface PluginContext {
2471
2854
  * read as though it did.
2472
2855
  */
2473
2856
  workspaces?: AgentWorkspaces;
2857
+ /**
2858
+ * Where this plugin may keep durable state. See {@link PluginStateDirectory}.
2859
+ *
2860
+ * A host that omits it gives the plugin nowhere to remember anything
2861
+ * across a restart. Such a plugin must say what it gives up — a channel
2862
+ * that cannot store its read position cannot promise a message is handled
2863
+ * once — and must never choose a directory of its own instead.
2864
+ */
2865
+ stateDirectory?: PluginStateDirectory;
2474
2866
  /**
2475
2867
  * The host's log, for what a plugin has to say after `setup` returns —
2476
2868
  * a server that dropped, a reconnect that failed. The daemon's is the
@@ -2549,6 +2941,15 @@ export interface SkillDescriptor {
2549
2941
  * else, per turn and in memory.
2550
2942
  */
2551
2943
  export interface Skill extends SkillDescriptor {
2944
+ /**
2945
+ * Ships with Stratus and is enabled for every agent, whatever its soul's
2946
+ * `skills:` says. Reserved for the system's own documentation (the
2947
+ * `stratus` skill; `SkillRegistry.register` refuses it on any other id): a skill from anywhere else changing how an agent
2948
+ * behaves is opted into, but an agent that cannot read how the system it
2949
+ * runs in works guesses instead, the way one suggested `passEnv` for a
2950
+ * secret after reading the install around it.
2951
+ */
2952
+ builtin?: boolean;
2552
2953
  /**
2553
2954
  * Toolset globs this skill expects (`browser.*`, `fs.read`). Advisory: a
2554
2955
  * skill is prose and degrades, so an agent enabling one without the tools
@@ -2633,6 +3034,12 @@ export declare class SkillRegistry {
2633
3034
  * A read already underway keeps the promise it holds either way. `next`
2634
3035
  * is consumed — emptied, not shared — so nothing can go on mutating this
2635
3036
  * registry through it.
3037
+ *
3038
+ * A built-in skill serving now is never dropped by the swap: a `next`
3039
+ * without one (its file unreadable mid-reload) keeps the one serving,
3040
+ * and a bare alias `next` gave its id yields to it. A reload is
3041
+ * all-or-nothing for what it read; it must not take away from every
3042
+ * agent the one skill no soul can opt back into.
2636
3043
  */
2637
3044
  replaceWith(next: SkillRegistry): void;
2638
3045
  describe(): SkillDescriptor[];
@@ -2644,6 +3051,12 @@ export declare class SkillRegistry {
2644
3051
  read(id: string): Promise<string>;
2645
3052
  }
2646
3053
  export declare const SKILL_READ_TOOL_NAME = "skill.read";
3054
+ /**
3055
+ * The built-in skill's id: how Stratus works, enabled for every agent.
3056
+ * Reserved, so an operator directory or a plugin cannot put a different
3057
+ * procedure under the name every agent is pointed at.
3058
+ */
3059
+ export declare const STRATUS_SKILL_ID = "stratus";
2647
3060
  export interface SkillReadToolOptions {
2648
3061
  /**
2649
3062
  * Which `skills:` entries the session's agent is held to. Defaults to the
@@ -2727,6 +3140,43 @@ export declare const renderMemorySection: (memory: readonly MemoryEntry[] | Memo
2727
3140
  * here may ever include a body.
2728
3141
  */
2729
3142
  export declare const renderSkillsSection: (skills: readonly SkillDescriptor[] | undefined) => string | undefined;
3143
+ /**
3144
+ * The language agents write in when nothing says otherwise: American
3145
+ * English. A soul's `language:` and the config file's `language` override
3146
+ * it; see `resolveRuntimeConfig`, which decides between those two.
3147
+ */
3148
+ export declare const DEFAULT_LANGUAGE = "en-US";
3149
+ /** Whether a `language` setting is a BCP 47 tag; see `canonicalLanguageTag`. */
3150
+ export declare const isLanguageTag: (value: unknown) => value is string;
3151
+ /**
3152
+ * How to reply, told to every agent before its persona.
3153
+ *
3154
+ * Models default to the long, headed, bulleted answer, and an agent is read
3155
+ * mostly in chat, often on a phone, where that is the wrong shape. Souls
3156
+ * were each growing their own copy of these rules, so they live here once.
3157
+ * Short must not turn into incomplete, so the same section says a requested
3158
+ * deliverable arrives whole and a blocker is always said. A long one goes
3159
+ * where the reader can open it: an agent on Slack that answers with a path
3160
+ * on the daemon's disk has handed over nothing, so when sharing is not
3161
+ * possible the content itself goes in the reply.
3162
+ *
3163
+ * The accuracy rules are here for the same reason, each after an incident:
3164
+ * an agent with no Slack tool insisted it had no Slack connection, and
3165
+ * agents reported work as done that had only been drafted. The language
3166
+ * rule is here because the default persona is not: a custom soul replaces
3167
+ * it, and an agent wrote "favour" and "licence" into American website copy
3168
+ * because nothing shared said which English. A pasted British paragraph is
3169
+ * not a setting, so the rule says a task or a client's style guide decides
3170
+ * one deliverable and nothing is inferred from source text.
3171
+ *
3172
+ * The last sentence lets the soul and the person override length, format,
3173
+ * and language, and deliberately nothing else: a style preference never
3174
+ * changes what an agent may do, and "be brief" must not read as "skip the
3175
+ * blocker". Written without em dashes on purpose: a model imitates the
3176
+ * prose it is prompted with, and a rule against them set in them undoes
3177
+ * itself.
3178
+ */
3179
+ export declare const renderReplySection: (language: string | undefined) => string;
2730
3180
  /**
2731
3181
  * Where the conversation is happening, for a session a channel started.
2732
3182
  *
@@ -2734,11 +3184,39 @@ export declare const renderSkillsSection: (skills: readonly SkillDescriptor[] |
2734
3184
  * dispatches (`slack` today). Without this the agent could not tell: an
2735
3185
  * agent with no Slack tool, asked about an attachment in a Slack DM, told
2736
3186
  * the person it had "no Slack connection at all" and argued the point.
2737
- * Rendered from the session rather than handed in per turn so sessions
2738
- * opened before this existed get it too. The name must look like a channel
3187
+ * The channel is read from the session, so sessions opened before this
3188
+ * existed get it too; the room (`ConversationContext`) comes per turn from
3189
+ * the runtime, falling back to what the session's first turn recorded, so
3190
+ * an agent in a Slack DM no longer tells the person they are "talking on
3191
+ * the terminal". The name must look like a channel
2739
3192
  * id, since it is interpolated into the prompt.
3193
+ *
3194
+ * What it says about attachments is what the adapter writes into the
3195
+ * message, not a promise that they arrive: it used to say that what people
3196
+ * attach "reaches you", which read as though every file's contents did,
3197
+ * when most reach the turn as a name only. The Slack adapter marks each
3198
+ * file as read, shown, or unread with its reason; the section sends the
3199
+ * model to that note and nowhere else.
2740
3200
  */
2741
- export declare const renderChannelSection: (session: Pick<Session, "metadata">) => string | undefined;
3201
+ export declare const renderChannelSection: (session: Pick<Session, "metadata">, conversation?: ConversationContext) => string | undefined;
3202
+ /**
3203
+ * How this agent is run, for a host that said (see
3204
+ * {@link AgentRuntimeContext}).
3205
+ *
3206
+ * It says the soul is already in the prompt as firmly as it says where the
3207
+ * file is. An agent told only a path treats it as something to go and read,
3208
+ * and it usually cannot: the soul sits beside its state directory, outside
3209
+ * any root an operator hands `tool-fs`, and it has to stay there, since
3210
+ * the soul is what grants the agent its tools and credentials.
3211
+ *
3212
+ * Credentials get the same treatment, and for the same reason: an agent
3213
+ * whose operator had stored a shared key went looking through its files and
3214
+ * its environment for it. A named credential only ever reaches a plugin tool
3215
+ * that declared it, so the section names what the soul grants and says there
3216
+ * is nothing to find. Names only; a value never comes near a prompt.
3217
+ * Written without em dashes for the reason the reply section is.
3218
+ */
3219
+ export declare const renderRuntimeSection: (runtime: AgentRuntimeContext | undefined, agent: Pick<AgentDefinition, "credentials">) => string | undefined;
2742
3220
  export interface SystemPromptOptions {
2743
3221
  /** Host-level preamble, rendered before the agent's own persona. */
2744
3222
  preamble?: string;
@@ -2749,12 +3227,12 @@ export interface SystemPromptOptions {
2749
3227
  * Which part of what an agent is told a section is.
2750
3228
  *
2751
3229
  * The distinction a caller actually needs is stable versus volatile:
2752
- * `preamble`, `replies`, `persona`, `channel`, and `skills` are byte-identical across every turn of
3230
+ * `preamble`, `replies`, `persona`, `runtime`, `channel`, and `skills` are byte-identical across every turn of
2753
3231
  * an agent's life, while `memory` is rewritten whenever the agent remembers
2754
3232
  * anything. A provider that caches its request prefix has to place those two
2755
3233
  * groups differently, and it cannot tell them apart from rendered strings.
2756
3234
  */
2757
- export type SystemPromptSectionKind = 'preamble' | 'replies' | 'persona' | 'channel' | 'memory' | 'skills';
3235
+ export type SystemPromptSectionKind = 'preamble' | 'replies' | 'persona' | 'runtime' | 'channel' | 'memory' | 'skills';
2758
3236
  export interface SystemPromptSection {
2759
3237
  kind: SystemPromptSectionKind;
2760
3238
  text: string;
@@ -2773,11 +3251,11 @@ export interface SystemPromptSection {
2773
3251
  * The order is the order. This is the one rule about what an agent is told;
2774
3252
  * `renderSystemPromptSections` is the same rule with the labels dropped.
2775
3253
  */
2776
- export declare const renderSystemPromptParts: (request: Pick<ProviderRequest, "session" | "memory" | "skills">, options?: SystemPromptOptions) => SystemPromptSection[];
3254
+ export declare const renderSystemPromptParts: (request: Pick<ProviderRequest, "session" | "memory" | "skills" | "runtime">, options?: SystemPromptOptions) => SystemPromptSection[];
2777
3255
  /** The sections as plain strings, in the same order. */
2778
- export declare const renderSystemPromptSections: (request: Pick<ProviderRequest, "session" | "memory" | "skills">, options?: SystemPromptOptions) => string[];
3256
+ export declare const renderSystemPromptSections: (request: Pick<ProviderRequest, "session" | "memory" | "skills" | "runtime">, options?: SystemPromptOptions) => string[];
2779
3257
  /** The sections joined the way single-string providers send them. */
2780
- export declare const renderSystemPrompt: (request: Pick<ProviderRequest, "session" | "memory" | "skills">, options?: SystemPromptOptions) => string | undefined;
3258
+ export declare const renderSystemPrompt: (request: Pick<ProviderRequest, "session" | "memory" | "skills" | "runtime">, options?: SystemPromptOptions) => string | undefined;
2781
3259
  /**
2782
3260
  * The two producer channels, collected for one call: the tool's standing
2783
3261
  * `outputTrust` declaration and whatever the tool marks through the
@@ -2889,7 +3367,31 @@ export interface RunInput {
2889
3367
  * attachments instead, as an overheard one does.
2890
3368
  */
2891
3369
  addressed?: boolean;
3370
+ /**
3371
+ * The caller's name for this dispatch, stored on the user message
3372
+ * (`Message.idempotencyKey`) in the same write as the message itself.
3373
+ * The runner only records it; refusing a repeat is the host's job — see
3374
+ * `workItemState` and the gateway's `DispatchInput.idempotencyKey`.
3375
+ */
3376
+ idempotencyKey?: string;
3377
+ /**
3378
+ * Whether this turn may run on a provider that hosts its own tool loop,
3379
+ * stored on the user message (`Message.hostedLoop`). The host knows what
3380
+ * it resolved for the turn; the runner only records it.
3381
+ */
3382
+ hostedLoop?: boolean;
2892
3383
  metadata?: JsonObject;
3384
+ /**
3385
+ * What the host can say about how this agent is run, rendered as the
3386
+ * `runtime` section; see {@link AgentRuntimeContext}. Omitted, the
3387
+ * section is not rendered.
3388
+ *
3389
+ * Per run rather than a runner option, because the answer belongs to the
3390
+ * snapshot the host dispatched with: a daemon that re-reads the roster
3391
+ * between two awaits would otherwise tell an agent running on one soul
3392
+ * file that another file is its soul.
3393
+ */
3394
+ runtime?: AgentRuntimeContext;
2893
3395
  /** Aborting fails the turn cleanly; see RunAbortedError. */
2894
3396
  signal?: AbortSignal;
2895
3397
  }
@@ -2900,6 +3402,10 @@ export interface ResumeInput {
2900
3402
  images?: ImageAttachment[];
2901
3403
  /** See `RunInput.addressed`. */
2902
3404
  addressed?: boolean;
3405
+ /** See `RunInput.idempotencyKey`. */
3406
+ idempotencyKey?: string;
3407
+ /** See `RunInput.hostedLoop`. */
3408
+ hostedLoop?: boolean;
2903
3409
  /**
2904
3410
  * This turn's metadata — read for the sender's trust
2905
3411
  * (`SENDER_TRUST_METADATA_KEY`) and not merged into the session's. The
@@ -2908,6 +3414,8 @@ export interface ResumeInput {
2908
3414
  * trust.
2909
3415
  */
2910
3416
  metadata?: JsonObject;
3417
+ /** See `RunInput.runtime`. */
3418
+ runtime?: AgentRuntimeContext;
2911
3419
  /** Aborting fails the turn cleanly; see RunAbortedError. */
2912
3420
  signal?: AbortSignal;
2913
3421
  }
@@ -3104,6 +3612,30 @@ export declare class AgentRunner {
3104
3612
  */
3105
3613
  private labelLegacySession;
3106
3614
  resume(input: ResumeInput): Promise<Session>;
3615
+ /**
3616
+ * Picks up a turn whose process died while it was `running`: its user
3617
+ * message is durable and its reply is not. Nothing is appended — the turn
3618
+ * already has its message — so this is `resume` without the input.
3619
+ * Dangling tool calls are closed as interrupted, telling the model they
3620
+ * may not have run rather than running them again, and the loop goes back
3621
+ * to the provider with the transcript as it stands.
3622
+ *
3623
+ * A transcript that already ends in the reply (the response was saved,
3624
+ * and the process died before the save that marks the turn completed) is
3625
+ * completed as it stands, with no provider call: asking again would send
3626
+ * a second answer to one message.
3627
+ *
3628
+ * Marks the turn's message `continuedAfterCrash` in its first save, so a
3629
+ * host can refuse to do this twice. Resolves `undefined` when the session
3630
+ * is missing or no longer `running`. Never for a turn parked on a human:
3631
+ * `recoverPendingApproval` re-enters the parked call, which reconciling
3632
+ * here would close.
3633
+ */
3634
+ continueTurn(sessionId: string, options?: {
3635
+ /** See `RunInput.runtime`. */
3636
+ runtime?: AgentRuntimeContext;
3637
+ signal?: AbortSignal;
3638
+ }): Promise<Session | undefined>;
3107
3639
  /**
3108
3640
  * A message reaching the session with no turn run on it: something said
3109
3641
  * in a conversation the agent is in, to somebody else. Appended and
@@ -3122,11 +3654,18 @@ export declare class AgentRunner {
3122
3654
  */
3123
3655
  observe(input: ObserveInput): Promise<Session>;
3124
3656
  /**
3125
- * Appends a synthetic failed result directly after every tool call that
3126
- * has none — the durable trace of a turn interrupted between the call's
3127
- * save and its result's. The model sees an honest record ("interrupted,
3128
- * never ran to completion") instead of a wire-format violation, and a
3129
- * resume can decide to retry rather than assume the side effect landed.
3657
+ * Adds a synthetic failed result for every tool call that has none — the
3658
+ * durable trace of a turn interrupted between the call's save and its
3659
+ * result's. The model sees an honest record ("interrupted, never ran to
3660
+ * completion") instead of a wire-format violation, and a resume can
3661
+ * decide to retry rather than assume the side effect landed.
3662
+ *
3663
+ * Placed where an uninterrupted turn would have written it: after the
3664
+ * whole response the call came in — every assistant message that one
3665
+ * save wrote — and after any results already recorded for it. Directly
3666
+ * after the call would split a response whose text followed its call, and
3667
+ * a provider replaying the response's raw turn (which already holds that
3668
+ * text) would then send the text a second time.
3130
3669
  */
3131
3670
  private reconcileInterruptedToolCalls;
3132
3671
  /**
@@ -3163,11 +3702,14 @@ export declare class AgentRunner {
3163
3702
  * the allowlist actually granted (canonical before alias) — the id the
3164
3703
  * model is told is the id `skill.read` will accept.
3165
3704
  *
3166
- * Empty for an agent whose soul has no `skills:` key, which is what keys
3167
- * both `skill.read` gates: no skills, no reader.
3705
+ * A built-in skill is in it for every agent; otherwise it is empty for
3706
+ * an agent whose soul has no `skills:` key, which is what keys both
3707
+ * `skill.read` gates: no skills, no reader.
3168
3708
  */
3169
3709
  private enabledSkillsFor;
3170
3710
  private executeTurns;
3711
+ /** See `completeAnsweredTurn`; the loop's end and `continueTurn`'s. */
3712
+ private completeTurn;
3171
3713
  /**
3172
3714
  * Finishes a turn that was parked on a human when the process died.
3173
3715
  *
@@ -3188,6 +3730,8 @@ export declare class AgentRunner {
3188
3730
  * about at all, so they still face the policy normally.
3189
3731
  */
3190
3732
  denyPending?: boolean;
3733
+ /** See `RunInput.runtime`. */
3734
+ runtime?: AgentRuntimeContext;
3191
3735
  signal?: AbortSignal;
3192
3736
  }): Promise<Session | undefined>;
3193
3737
  /**