@stratusagent/core 0.11.4 → 0.11.6

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
@@ -672,15 +672,75 @@ export interface ToolDescriptor {
672
672
  /** Carried so providers hosting their own loop can see it too. */
673
673
  risk?: ToolRisk;
674
674
  }
675
+ /**
676
+ * What sort of thing a fact is. One flat bucket is why a long-lived store
677
+ * goes noisy: these have different useful lifetimes and different injection
678
+ * value, and no policy can tell them apart from prose.
679
+ *
680
+ * - `semantic` — a fact about the world.
681
+ * - `episodic` — a thing that happened.
682
+ * - `procedural` — how something is done.
683
+ * - `preference` — how the people it works with want things.
684
+ */
685
+ export type MemoryEntryKind = 'semantic' | 'episodic' | 'procedural' | 'preference';
686
+ /** Every kind, in the order the tool description lists them. */
687
+ export declare const MEMORY_ENTRY_KINDS: readonly ["semantic", "episodic", "procedural", "preference"];
688
+ export declare const isMemoryEntryKind: (value: unknown) => value is MemoryEntryKind;
675
689
  /**
676
690
  * One remembered fact. Memory is keyed by agent, never by session or
677
691
  * channel, so an agent carries the same knowledge everywhere it appears.
692
+ *
693
+ * Everything past the first four fields is optional and additive, and that
694
+ * is a promise rather than an accident: a JSONL line carrying only `id`,
695
+ * `agentId`, `content`, and `createdAt` still loads, is still recallable,
696
+ * and still reaches the prompt. A richer shape is the thing most likely to
697
+ * break the hand-edit promise, so nothing here may ever become required.
678
698
  */
679
699
  export interface MemoryEntry {
680
700
  id: string;
681
701
  agentId: string;
682
702
  content: string;
683
703
  createdAt: string;
704
+ /** What sort of fact this is — see `MemoryEntryKind`. Absent means nobody said. */
705
+ kind?: MemoryEntryKind;
706
+ /**
707
+ * The entities this fact concerns, as the writer named them. This is what
708
+ * turns "everything I know about the deploy pipeline" into a lookup rather
709
+ * than a guess at query terms, and it is what the prompt's topic index is
710
+ * built from.
711
+ *
712
+ * It also *participates in matching*, in every store: an index block that
713
+ * advertised a topic the search could not find would invite exactly the
714
+ * query it then missed. `memoryEntryTokens` is where that is written once.
715
+ */
716
+ about?: string[];
717
+ /**
718
+ * When the fact starts and stops being true — validity time, a different
719
+ * axis from `createdAt`, which is transaction time. Conflating the two is
720
+ * why assistants confidently report where someone used to work.
721
+ *
722
+ * Both bounds are ISO-8601 instants; `validUntil` is the moment the fact
723
+ * stops holding, exclusive. Outside the window an entry leaves what is
724
+ * true now — the pinned core, the topic index, the recency tail — and
725
+ * stays findable by `search`, marked. That is one rule for both bounds:
726
+ * a `validFrom` in the future is the same error running the other way,
727
+ * and an entry that is not true *yet* reaching the prompt would be
728
+ * presented as true now.
729
+ */
730
+ validFrom?: string;
731
+ validUntil?: string;
732
+ /**
733
+ * The id of the entry this one replaces. The successor carries the
734
+ * retirement itself — one appended record, never a tombstone plus an
735
+ * entry, because the store's only atomicity is one `O_APPEND` record and
736
+ * stopping between two of them loses a fact.
737
+ *
738
+ * The retirement is scoped to *this* entry's validity window: a successor
739
+ * dated from next Monday leaves its predecessor standing until then, and
740
+ * an already-expired successor displaces nothing. See
741
+ * `supersededMemoryIdsAt`.
742
+ */
743
+ supersedes?: string;
684
744
  metadata?: JsonObject;
685
745
  /**
686
746
  * Where the fact came from — the least trusted thing the writing session
@@ -699,6 +759,21 @@ export interface MemoryEntry {
699
759
  * reading the store, and travels with the entry as ordinary data.
700
760
  */
701
761
  origin?: MemoryOrigin;
762
+ /**
763
+ * How often this entry has been read back, and when last — **derived, and
764
+ * never part of the record**. These are observations about *reading*, not
765
+ * facts the agent learned, so they live in whatever index a store keeps
766
+ * and a store that keeps none simply omits them. The consequence is exact
767
+ * and is the documented one: deleting the index loses your usage
768
+ * statistics, never your memories. Ranking may read them; nothing
769
+ * deletes on them.
770
+ */
771
+ usage?: MemoryUsage;
772
+ }
773
+ /** See `MemoryEntry.usage`. Counted by the store on each `search` that returns the entry. */
774
+ export interface MemoryUsage {
775
+ recallCount: number;
776
+ lastRecalledAt: string;
702
777
  }
703
778
  export interface MemoryOrigin {
704
779
  sessionId?: string;
@@ -737,16 +812,60 @@ export declare const MEMORY_ENTRY_MAX_BYTES = 4096;
737
812
  * several cap-size entries still fit (see `MEMORY_ENTRY_MAX_BYTES`).
738
813
  */
739
814
  export declare const MEMORY_READ_MAX_BYTES = 16384;
815
+ /**
816
+ * The recall tool's name, defined here because the prompt's topic index
817
+ * tells the agent what to call — a block advertising a tool by a name the
818
+ * registry does not use would be a dead pointer. `@stratusagent/agents`
819
+ * builds the tool itself and re-exports this as `RECALL_TOOL_NAME`.
820
+ */
821
+ export declare const MEMORY_RECALL_TOOL_NAME = "memory.recall";
740
822
  /** Results a `search` returns when the caller names no limit. */
741
823
  export declare const MEMORY_RECALL_DEFAULT_LIMIT = 10;
742
824
  /** Ceiling a `search` limit is clamped to, whatever the caller asked for. */
743
825
  export declare const MEMORY_RECALL_MAX_LIMIT = 50;
744
- /** How many recent entries the runner injects into the system prompt. */
745
- export declare const MEMORY_INJECTION_LIMIT = 20;
826
+ /**
827
+ * The pinned core's hard cap, in UTF-8 bytes of content. It **refuses
828
+ * rather than evicts**, on the same reasoning as the per-entry cap: a
829
+ * pinned set that silently drops its oldest member is a pin that does not
830
+ * mean anything.
831
+ */
832
+ export declare const MEMORY_PINNED_MAX_BYTES = 2048;
833
+ /**
834
+ * The topic index's budget, in UTF-8 bytes of rendered block. Roughly a
835
+ * thousand tokens: enough to tell the agent the shape of its own store,
836
+ * which is what turns `memory.recall` from a guess into a targeted read.
837
+ */
838
+ export declare const MEMORY_INDEX_MAX_BYTES = 4096;
839
+ /**
840
+ * The recency tail — deliberately smaller than the twenty entries that
841
+ * were the whole of injection before the pinned core and the topic index
842
+ * existed, so a cold store with nothing pinned and nothing indexed still
843
+ * behaves roughly as it did, and a warm one spends its budget on the two
844
+ * blocks that are correlated with what the agent knows.
845
+ */
846
+ export declare const MEMORY_RECENCY_INJECTION_LIMIT = 8;
847
+ /** Byte budget for the recency tail, applied like every other bounded read. */
848
+ export declare const MEMORY_RECENCY_MAX_BYTES = 4096;
746
849
  /** UTF-8 size of an entry's content — the unit both memory caps are stated in. */
747
850
  export declare const memoryContentByteLength: (content: string) => number;
748
851
  /** Refuses (never truncates) content over the per-entry cap: a half-stored fact is worse than a refused one. */
749
852
  export declare const assertMemoryContentWithinCap: (content: string) => void;
853
+ /**
854
+ * How many entities one fact may be filed under, and how much room their
855
+ * names get. `about` is model-written and would otherwise be the one
856
+ * unbounded field on an entry: the per-entry cap covers `content` alone, so
857
+ * without this a single `memory.remember` could write a megabyte of entity
858
+ * names into the record and into every store's search index. The prompt's
859
+ * topic block has its own budget and would survive it; the record would
860
+ * not, and the record is the thing that has to stay hand-editable.
861
+ *
862
+ * Small on purpose. A fact about a dozen things is a fact that has not been
863
+ * written down properly yet.
864
+ */
865
+ export declare const MEMORY_ABOUT_MAX_KEYS = 12;
866
+ export declare const MEMORY_ABOUT_MAX_BYTES = 512;
867
+ /** Refuses, like the content cap, and for the same reason: a half-filed fact is worse than a refused one. */
868
+ export declare const assertMemoryAboutWithinCap: (about: readonly string[]) => void;
750
869
  /**
751
870
  * The tokenizer both store implementations share: NFC-normalized,
752
871
  * case-folded, split on anything that is not a Unicode letter or digit.
@@ -756,12 +875,72 @@ export declare const assertMemoryContentWithinCap: (content: string) => void;
756
875
  */
757
876
  export declare const tokenizeMemoryText: (text: string) => string[];
758
877
  /**
759
- * Whether content matches a tokenized query: every query token must be
760
- * present as a whole token. A query that tokenized to nothing matches
761
- * nothing — recall with no searchable terms is a question that was not
762
- * asked, not a request for everything.
878
+ * An entry's `about` keys, cleaned the one way every consumer reads them:
879
+ * trimmed, empty ones dropped, duplicates collapsed case-insensitively
880
+ * with the first spelling kept. A hand-edited line whose `about` is not an
881
+ * array of strings contributes nothing rather than throwing — the record
882
+ * is the operator's to edit, and a bad key is not worth losing a fact over.
883
+ */
884
+ export declare const memoryEntryAbout: (entry: Pick<MemoryEntry, "about">) => string[];
885
+ /**
886
+ * Everything about an entry a query is matched against: its content and its
887
+ * `about` keys, through the shared tokenizer.
888
+ *
889
+ * `about` participating is the point rather than an edge. An entry reading
890
+ * "it now uses Postgres" with `about: ["deploy pipeline"]` is listed under
891
+ * that topic in the prompt's index, and a store matching content alone
892
+ * would then miss the very query the listing invites — an alias in `about`
893
+ * against a pronoun in `content` is the case the field exists to serve.
894
+ * Written once here so the FTS `tokens` column and the in-memory filter
895
+ * cannot disagree about what a store holds.
896
+ */
897
+ export declare const memoryEntryTokens: (entry: Pick<MemoryEntry, "content" | "about">) => string[];
898
+ /**
899
+ * Whether an entry matches a tokenized query: every query token must be
900
+ * present as a whole token of its content or of one of its `about` keys. A
901
+ * query that tokenized to nothing matches nothing — recall with no
902
+ * searchable terms is a question that was not asked, not a request for
903
+ * everything.
763
904
  */
764
- export declare const memoryQueryMatches: (content: string, queryTokens: readonly string[]) => boolean;
905
+ export declare const memoryQueryMatches: (entry: Pick<MemoryEntry, "content" | "about">, queryTokens: readonly string[]) => boolean;
906
+ /**
907
+ * Where an entry sits relative to its validity window at a given instant.
908
+ * `current` is the only status that is true *now*; both others leave the
909
+ * injected slice and stay findable by `search`, marked.
910
+ */
911
+ export type MemoryValidity = 'current' | 'not-yet-valid' | 'expired';
912
+ /**
913
+ * One rule, both bounds: a fact holds from `validFrom` (inclusive) until
914
+ * `validUntil` (exclusive). Pure in the entry and the clock, so both store
915
+ * implementations compute the same answer from the record alone.
916
+ */
917
+ export declare const memoryValidityAt: (entry: Pick<MemoryEntry, "validFrom" | "validUntil">, at: Date) => MemoryValidity;
918
+ /** Shorthand for the only status that is true now. */
919
+ export declare const isMemoryEntryCurrent: (entry: Pick<MemoryEntry, "validFrom" | "validUntil">, at: Date) => boolean;
920
+ /**
921
+ * The ids retired by supersession at a given instant, over one agent's
922
+ * un-tombstoned entries.
923
+ *
924
+ * An entry is retired exactly while a successor of it is *current*, which
925
+ * is the only reading that keeps supersession and validity from cancelling
926
+ * each other: a successor dated from next Monday retiring its predecessor
927
+ * today would leave neither fact available as true now — the old one
928
+ * retired, the new one excluded as not-yet-valid — so the agent forgets
929
+ * something it still believes and gains nothing for it.
930
+ *
931
+ * Two successors of the same entry both retire it, in either order, and
932
+ * both stay live. The invariant is the *retirement*, not a unique
933
+ * replacement: each successor is a fact the agent wrote, and retiring one
934
+ * to make the other unique would delete a write nobody asked to retract.
935
+ * Two live successors that disagree are a contradiction in content, which
936
+ * is the maintenance pass's job and not a storage race.
937
+ *
938
+ * Callers pass the agent's un-tombstoned entries, so forgetting a successor
939
+ * releases the entry it retired — a revision the agent took back leaves the
940
+ * fact it replaced standing, which is the recoverable direction in a store
941
+ * whose whole posture is that nothing is lost.
942
+ */
943
+ export declare const supersededMemoryIdsAt: (entries: readonly MemoryEntry[], at: Date) => Set<string>;
765
944
  /**
766
945
  * Recall order — newest first, ties on `createdAt` broken by entry id
767
946
  * ascending. The tie-break is not about frequency: without one, two
@@ -772,10 +951,30 @@ export declare const memoryQueryMatches: (content: string, queryTokens: readonly
772
951
  export declare const compareMemoryRecallOrder: (a: MemoryEntry, b: MemoryEntry) => number;
773
952
  /** Chronological order (oldest first), same tie-break — how a bounded `list` presents its slice. */
774
953
  export declare const compareMemoryChronology: (a: MemoryEntry, b: MemoryEntry) => number;
954
+ /**
955
+ * How a read orders what it found. Named in the request and reported in the
956
+ * result, rather than frozen in the contract.
957
+ *
958
+ * `recency` is mandatory, is the default, and is what the parity tests
959
+ * assert. `relevance` and `hybrid` are optional: a store that lacks the one
960
+ * asked for **serves `recency` and says so** rather than erroring, so two
961
+ * implementations stay honest about ordering without memory being frozen at
962
+ * clock order forever — and an embeddings-backed store behind a plugin seam
963
+ * becomes an additive capability rather than a contract break.
964
+ */
965
+ export type MemoryRankingStrategy = 'recency' | 'relevance' | 'hybrid';
966
+ export declare const MEMORY_RANKING_STRATEGIES: readonly ["recency", "relevance", "hybrid"];
967
+ export declare const isMemoryRankingStrategy: (value: unknown) => value is MemoryRankingStrategy;
775
968
  export interface MemoryReadResult {
776
969
  entries: MemoryEntry[];
777
970
  /** True when live entries beyond these existed — the entry limit or the byte budget bound, whichever bit first. */
778
971
  truncated: boolean;
972
+ /**
973
+ * The ordering the store actually applied. Present on a `search`, where
974
+ * the caller may have asked for one the store does not implement; absent
975
+ * on `list`, which is chronological by definition.
976
+ */
977
+ strategy?: MemoryRankingStrategy;
779
978
  }
780
979
  export interface MemoryListOptions {
781
980
  /**
@@ -786,6 +985,53 @@ export interface MemoryListOptions {
786
985
  * feed a model always pass a bound.
787
986
  */
788
987
  limit?: number;
988
+ /**
989
+ * Which entries count as live for this read. `current` — the default,
990
+ * and what "what is true now" means — drops entries outside their
991
+ * validity window; `all` keeps them, for the operator views that exist
992
+ * to show an expired fact *as* expired. Superseded and forgotten entries
993
+ * are out of both: those are retirements, not calendar questions.
994
+ */
995
+ validity?: 'current' | 'all';
996
+ }
997
+ /** Which pins a read wants — see `AgentMemoryStore.pinned`. */
998
+ export interface MemoryPinnedOptions {
999
+ /**
1000
+ * `current`, the default, is the pinned core as the prompt carries it:
1001
+ * live and true now. `allocated` is everything **holding budget** —
1002
+ * superseded and out-of-window pins included — which is a different
1003
+ * question and the one an operator, or a caller re-allocating a merged
1004
+ * budget, actually has. A pin nothing admits exists is one nobody can
1005
+ * unpin, and it is exactly what makes a later pin refuse.
1006
+ */
1007
+ include?: 'current' | 'allocated';
1008
+ }
1009
+ export interface MemorySearchOptions {
1010
+ /** Clamped by `clampMemoryRecallLimit`; the store bounds it either way. */
1011
+ limit?: number;
1012
+ /** The ordering asked for. See `MemoryRankingStrategy` — an unsupported one is served as `recency`. */
1013
+ strategy?: MemoryRankingStrategy;
1014
+ }
1015
+ /**
1016
+ * What a writer says about a new entry beyond its text. Every field is
1017
+ * optional, and an `append` that passes none writes exactly the four-field
1018
+ * line an older build would have written.
1019
+ */
1020
+ export interface MemoryAppendOptions {
1021
+ metadata?: JsonObject;
1022
+ provenance?: MemoryProvenance;
1023
+ kind?: MemoryEntryKind;
1024
+ about?: string[];
1025
+ validFrom?: string;
1026
+ validUntil?: string;
1027
+ /**
1028
+ * The entry this one replaces. The store resolves it against the
1029
+ * caller's **own** live entries before anything is appended and throws
1030
+ * otherwise: the record lane is shared across agents, and a writer that
1031
+ * skipped the check would let one agent retire another's memory by
1032
+ * naming its id.
1033
+ */
1034
+ supersedes?: string;
789
1035
  }
790
1036
  /** Clamp a caller-chosen search limit into the store's own bounds. */
791
1037
  export declare const clampMemoryRecallLimit: (limit?: number) => number;
@@ -803,7 +1049,130 @@ export declare const boundMemoryRead: (candidates: readonly MemoryEntry[], limit
803
1049
  * presentation order are different things; conflating them is the bug this
804
1050
  * helper exists so nobody writes three times.
805
1051
  */
806
- export declare const boundMemoryList: (candidates: readonly MemoryEntry[], limit: number) => MemoryReadResult;
1052
+ export declare const boundMemoryList: (candidates: readonly MemoryEntry[], limit: number, maxBytes?: number) => MemoryReadResult;
1053
+ /**
1054
+ * What the pin write path decides, and the sentence it says when it
1055
+ * refuses. Split out because the refusal has to name the cap — an operator
1056
+ * who cannot see why a pin bounced will assume it worked.
1057
+ */
1058
+ export interface MemoryPinOutcome {
1059
+ pinned: boolean;
1060
+ /** Present when `pinned` is false: what happened, as a full sentence. */
1061
+ reason?: string;
1062
+ /** Content bytes the effective pinned set holds after this call. */
1063
+ bytes: number;
1064
+ }
1065
+ /**
1066
+ * The pinned set the record produces, and the rule both stores replay it
1067
+ * by: **pins take effect in the order their records appear in the file,
1068
+ * until the cap, and the remainder are recorded but inert.**
1069
+ *
1070
+ * The write path refusing an over-cap pin is the common case and stays the
1071
+ * rule. This is for the race it cannot see: the CLI and the daemon build
1072
+ * separate stores over the same file and coordinate by `O_APPEND` alone, so
1073
+ * two processes pinning near the limit can both read the same total, both
1074
+ * accept, and both append. Replay gives them one effective set instead of
1075
+ * an injected slice that either overruns its budget or drops a pin by
1076
+ * whichever order it happened to read.
1077
+ *
1078
+ * **Append order, not `(createdAt, id)`.** A timestamp is the wrong key for
1079
+ * a budget: a pin written later with a skewed-earlier clock would sort
1080
+ * ahead of a pin that is already effective and consume the budget out from
1081
+ * under it, making an accepted pin inert after the fact and contradicting
1082
+ * the refusal the cap promises. `O_APPEND` already supplies a total order
1083
+ * no later writer can insert itself into. Recall ordering keeps its
1084
+ * `(createdAt, id)` rule — presenting facts newest-first is about meaning,
1085
+ * allocating a scarce budget is about arrival.
1086
+ *
1087
+ * Validity is deliberately *not* consulted here. The budget is a property
1088
+ * of the record, so it must not change when a clock ticks past a
1089
+ * `validUntil`; an out-of-window pin holds its place and is dropped from
1090
+ * the rendered core by `selectMemoryInjection`, which is where "true now"
1091
+ * is decided.
1092
+ */
1093
+ export declare const applyMemoryPinBudget: (pinnedIds: readonly string[], contentBytesFor: (id: string) => number | undefined, maxBytes?: number) => {
1094
+ effective: string[];
1095
+ inert: string[];
1096
+ bytes: number;
1097
+ };
1098
+ /** One line of the prompt's topic index: what the agent knows about, and how much. */
1099
+ export interface MemoryTopic {
1100
+ /** The `about` key, in the spelling the first entry to use it wrote. */
1101
+ name: string;
1102
+ count: number;
1103
+ /** The newest `createdAt` among the entries filed under it. */
1104
+ lastUpdatedAt: string;
1105
+ /**
1106
+ * The least trusted label among those entries. The topic index is a view
1107
+ * derived from entries, so it renders under the labelling invariant like
1108
+ * every other one: an entity name an `external` entry supplied must not
1109
+ * appear under the heading for the agent's own conclusions.
1110
+ */
1111
+ trust: TrustLevel;
1112
+ }
1113
+ /**
1114
+ * The topic index, built from `about` keys over the entries a store holds:
1115
+ * not facts, but what the agent *knows about*. Counts and last-updated,
1116
+ * ordered by count descending then name, so the block is stable across
1117
+ * turns and the biggest topics survive the budget.
1118
+ *
1119
+ * Shared rather than per-store for the usual reason: two implementations
1120
+ * building this differently would advertise different stores over the same
1121
+ * record.
1122
+ */
1123
+ export declare const collectMemoryTopics: (entries: readonly MemoryEntry[]) => MemoryTopic[];
1124
+ /**
1125
+ * The three bounded blocks a turn injects, in place of the one recency
1126
+ * slice memory used to send.
1127
+ *
1128
+ * They are three blocks *within* one `memory` section, never three sections
1129
+ * of their own — an adapter that places the volatile section separately
1130
+ * finds it by kind, so siblings sharing the kind would move one and drop
1131
+ * the rest of the request on the floor.
1132
+ */
1133
+ export interface MemoryInjection {
1134
+ /**
1135
+ * Always present when anything is pinned, hard-capped at
1136
+ * `MEMORY_PINNED_MAX_BYTES`. This is what makes an agent still know the
1137
+ * basics after a year with no search at all.
1138
+ */
1139
+ pinned: MemoryEntry[];
1140
+ /** What the agent knows about — the block that turns `memory.recall` from a guess into a targeted read. */
1141
+ topics: MemoryTopic[];
1142
+ /** The recency tail, so a cold store with nothing pinned behaves as it did before. */
1143
+ recent: MemoryEntry[];
1144
+ }
1145
+ /** Empty in all three blocks — what a host with no memory store injects. */
1146
+ export declare const emptyMemoryInjection: () => MemoryInjection;
1147
+ /**
1148
+ * Bound the three blocks against their budgets and keep them disjoint: a
1149
+ * pinned fact already in context is not worth a second copy in the tail,
1150
+ * and the tail is the block that shrinks, never the pinned core. The
1151
+ * pinned set is never pushed out by volume — that is the whole reason it
1152
+ * is a separate budget rather than a ranking bonus.
1153
+ */
1154
+ export declare const selectMemoryInjection: (input: {
1155
+ pinned: readonly MemoryEntry[];
1156
+ topics: readonly MemoryTopic[];
1157
+ recent: readonly MemoryEntry[];
1158
+ }) => MemoryInjection;
1159
+ /**
1160
+ * Read the three blocks out of a store. The policy — what counts as
1161
+ * current, what the budgets are, which block a fact lands in — lives in
1162
+ * `selectMemoryInjection` and the store's own `list`, so a store that
1163
+ * implements none of the optional methods still injects a recency tail and
1164
+ * nothing is special-cased at the call site.
1165
+ */
1166
+ export declare const buildMemoryInjection: (store: AgentMemoryStore, agentId: string) => Promise<MemoryInjection>;
1167
+ /**
1168
+ * The three blocks as a request may carry them: a bare array is the recency
1169
+ * tail alone, which is what `memory` meant before pinning and the topic
1170
+ * index existed and is still what a host building a request by hand may
1171
+ * pass.
1172
+ */
1173
+ export declare const asMemoryInjection: (memory: readonly MemoryEntry[] | MemoryInjection | undefined) => MemoryInjection;
1174
+ /** Every entry a turn injects, in the order they reach the prompt — what taints the session. */
1175
+ export declare const memoryInjectionEntries: (injection: readonly MemoryEntry[] | MemoryInjection | undefined) => MemoryEntry[];
807
1176
  /**
808
1177
  * Durable, searchable memory an agent writes and reads deliberately. The
809
1178
  * per-agent key is the access boundary — every method takes the agent id the
@@ -823,19 +1192,30 @@ export declare const boundMemoryList: (candidates: readonly MemoryEntry[], limit
823
1192
  * `MEMORY_READ_MAX_BYTES` when a limit is in play; `forget` tombstones
824
1193
  * rather than deletes, and `audit` is where tombstoned entries remain
825
1194
  * visible to an operator.
1195
+ * - An entry is **not live** when a tombstone retires it or a *current*
1196
+ * successor supersedes it (`supersededMemoryIdsAt`). An entry outside its
1197
+ * validity window is a different thing: still live, out of `list` by
1198
+ * default because it is not true now, and still found by `search` — where
1199
+ * the caller reads its status with `memoryValidityAt`.
826
1200
  */
827
1201
  export interface AgentMemoryStore {
828
1202
  /**
829
1203
  * Refuses content over `MEMORY_ENTRY_MAX_BYTES` rather than truncating it.
830
- * `provenance` is what the writer knows about where the fact came from;
831
- * an append without one stores an entry that reads as `unknown`, which
832
- * is the honest label for a writer that did not say.
1204
+ * `options.provenance` is what the writer knows about where the fact came
1205
+ * from; an append without one stores an entry that reads as `unknown`,
1206
+ * which is the honest label for a writer that did not say. Throws when
1207
+ * `options.supersedes` names an id that is not a live entry of this agent.
833
1208
  */
834
- append(agentId: string, content: string, metadata?: JsonObject, provenance?: MemoryProvenance): Promise<MemoryEntry>;
835
- /** Live entries, oldest first. See `MemoryListOptions` for the bounded form. */
1209
+ append(agentId: string, content: string, options?: MemoryAppendOptions): Promise<MemoryEntry>;
1210
+ /** Live entries, oldest first. See `MemoryListOptions` for the bounded and the out-of-window forms. */
836
1211
  list(agentId: string, options?: MemoryListOptions): Promise<MemoryReadResult>;
837
- /** Live entries matching the literal query, newest first, bounded. */
838
- search(agentId: string, query: string, limit?: number): Promise<MemoryReadResult>;
1212
+ /**
1213
+ * Live entries matching the literal query, newest first, bounded —
1214
+ * entries outside their validity window included, because keeping an
1215
+ * expired fact findable is the point of separating validity from
1216
+ * supersession. Matching covers content and `about` keys alike.
1217
+ */
1218
+ search(agentId: string, query: string, options?: MemorySearchOptions): Promise<MemoryReadResult>;
839
1219
  /** Tombstones a live entry. False when no live entry of this agent has that id. */
840
1220
  forget(agentId: string, entryId: string): Promise<boolean>;
841
1221
  /** The operator's audit read: every entry, tombstoned included, oldest first. */
@@ -852,21 +1232,160 @@ export interface AgentMemoryStore {
852
1232
  * operator has no way out of `unknown` but rewriting the record.
853
1233
  */
854
1234
  reassertTrust?(agentId: string, entryId: string, trust: TrustLevel): Promise<boolean>;
1235
+ /**
1236
+ * Pin a live entry into the core the prompt always carries. Refuses over
1237
+ * `MEMORY_PINNED_MAX_BYTES` naming the cap, and **never evicts**: a
1238
+ * pinned set that silently drops its oldest member is a pin that does not
1239
+ * mean anything. Like `supersedes`, it resolves the id against the
1240
+ * caller's own live entries before recording anything.
1241
+ *
1242
+ * Optional: a host whose store omits it gets no pinned core — injection
1243
+ * falls back to the topic index and the recency tail, which is what
1244
+ * memory did before pinning existed.
1245
+ */
1246
+ pin?(agentId: string, entryId: string): Promise<MemoryPinOutcome>;
1247
+ /** Undo a pin. False when this agent has no effective pin on that id. */
1248
+ unpin?(agentId: string, entryId: string): Promise<boolean>;
1249
+ /**
1250
+ * The effective pinned set, oldest first — see `applyMemoryPinBudget` for
1251
+ * what "effective" means, and `MemoryPinnedOptions` for the out-of-window
1252
+ * ones that hold budget without reaching the prompt.
1253
+ */
1254
+ pinned?(agentId: string, options?: MemoryPinnedOptions): Promise<MemoryEntry[]>;
1255
+ /**
1256
+ * What the agent knows *about*, from the `about` keys of everything true
1257
+ * now. Built with `collectMemoryTopics` so no two stores can advertise
1258
+ * the same record differently.
1259
+ *
1260
+ * Optional: a host whose store omits it gets no topic index, and its
1261
+ * agent is back to guessing query terms.
1262
+ */
1263
+ topics?(agentId: string): Promise<MemoryTopic[]>;
1264
+ /**
1265
+ * Append entries verbatim — ids, timestamps, kinds, validity and all —
1266
+ * under this agent's key. The operator's import path and the eval
1267
+ * harness's corpus loader; no tool reaches it, because an agent able to
1268
+ * choose its own entry ids could name one another record already refers
1269
+ * to.
1270
+ *
1271
+ * Entries whose id the agent already holds are skipped rather than
1272
+ * duplicated, so a re-run of an import is a no-op rather than a second
1273
+ * copy of everything.
1274
+ */
1275
+ importEntries?(agentId: string, entries: readonly MemoryEntry[]): Promise<MemoryImportResult>;
855
1276
  }
1277
+ /** What an import did: what landed, and what was already there. */
1278
+ export interface MemoryImportResult {
1279
+ imported: number;
1280
+ /** Ids skipped because this agent already holds an entry with that id. */
1281
+ skipped: string[];
1282
+ }
1283
+ /**
1284
+ * The refusal an over-cap pin says, naming the cap. An operator who cannot
1285
+ * see why a pin bounced will assume it worked, which is the failure the
1286
+ * refuse-never-evict rule exists to avoid in the first place.
1287
+ */
1288
+ export declare const pinnedCapRefusal: (heldBytes: number, wantedBytes: number) => string;
1289
+ /**
1290
+ * The other refusal, and the one a byte total cannot express: an inert pin
1291
+ * already stands in the record.
1292
+ *
1293
+ * `applyMemoryPinBudget` is a **prefix** rule, so once one pin overruns the
1294
+ * cap nothing recorded after it takes effect, whatever its size. A write
1295
+ * path that tested only the effective byte total would therefore accept a
1296
+ * small pin, append it, and report success for something `pinned` omits on
1297
+ * the very next read — an eviction wearing the refusal's clothes, which is
1298
+ * the one thing the cap promises cannot happen. The blocking id is named
1299
+ * because it is the only thing the operator can act on: the effective set
1300
+ * is under the cap and unpinning from *it* frees nothing.
1301
+ */
1302
+ export declare const pinnedInertRefusal: (blockingId: string) => string;
1303
+ /**
1304
+ * The write-path half of the per-agent boundary, shared by every store: an
1305
+ * id a new record names must resolve to a live entry the caller owns
1306
+ * *before* anything is appended.
1307
+ *
1308
+ * This is a security requirement rather than input hygiene. A store's
1309
+ * forgotten set is computed from the record without an agent filter, so a
1310
+ * writer that skipped this would let agent A retire agent B's memory by
1311
+ * naming its id — gone from B's `list`, its `search`, and its prompt, with
1312
+ * B's own `forget` never called.
1313
+ */
1314
+ export declare const assertSupersedableMemoryEntry: (live: readonly MemoryEntry[], entryId: string) => void;
1315
+ /**
1316
+ * An entry's optional fields as they are safe to read back from a record
1317
+ * nobody validated — a hand-edited line, an imported file, a newer build's
1318
+ * output. A field of the wrong shape is dropped rather than thrown over:
1319
+ * the record is the operator's to edit, and a bad `about` is not worth
1320
+ * losing a fact for. Written once so the file store's parser and the import
1321
+ * path cannot disagree about what a line means.
1322
+ */
1323
+ export declare const memoryEntryFields: (value: Pick<MemoryEntry, "kind" | "about" | "validFrom" | "validUntil" | "supersedes">) => Pick<MemoryEntry, "kind" | "about" | "validFrom" | "validUntil" | "supersedes">;
1324
+ /**
1325
+ * One entry as an import should store it: re-keyed to the importing agent,
1326
+ * optional fields validated, and `usage` dropped — usage is an observation
1327
+ * about reading this install did, never something a file carries in.
1328
+ */
1329
+ export declare const importableMemoryEntry: (entry: MemoryEntry, agentId: string) => MemoryEntry;
856
1330
  export declare class InMemoryAgentMemoryStore implements AgentMemoryStore {
857
1331
  private entries;
1332
+ /**
1333
+ * The pin lane, per agent, in append order — the same total order the
1334
+ * file store gets from `O_APPEND`, and for the same reason: a budget is
1335
+ * allocated by arrival, never by timestamp. A pin already held is not
1336
+ * re-appended, so re-pinning cannot move an entry to the back.
1337
+ */
1338
+ private pins;
1339
+ /**
1340
+ * Keyed by (agent, entry), never by entry alone: `importEntries`
1341
+ * preserves ids while re-keying entries, so two agents legitimately hold
1342
+ * the same id and one agent's reads must not move the other's counters.
1343
+ */
1344
+ private usage;
1345
+ /**
1346
+ * Every id this store holds, minted or imported. `append` skips past the
1347
+ * ones already taken, because an import that preserved `a:memory:1` and
1348
+ * left the counter at zero would mint that id again — and two entries
1349
+ * sharing an id are indistinguishable to tombstones, supersession, pins,
1350
+ * and usage alike.
1351
+ */
1352
+ private takenIds;
858
1353
  private counter;
859
1354
  private readonly now;
860
1355
  constructor(options?: {
861
1356
  now?: () => Date;
862
1357
  });
863
- append(agentId: string, content: string, metadata?: JsonObject, provenance?: MemoryProvenance): Promise<MemoryEntry>;
1358
+ append(agentId: string, content: string, options?: MemoryAppendOptions): Promise<MemoryEntry>;
1359
+ /** Not tombstoned and not superseded by a successor that is current at `at`. */
864
1360
  private live;
865
1361
  list(agentId: string, options?: MemoryListOptions): Promise<MemoryReadResult>;
866
- search(agentId: string, query: string, limit?: number): Promise<MemoryReadResult>;
1362
+ search(agentId: string, query: string, options?: MemorySearchOptions): Promise<MemoryReadResult>;
1363
+ private usageKey;
1364
+ private noteRecalled;
1365
+ /**
1366
+ * The entry a mutation may touch: live, which means un-tombstoned *and*
1367
+ * not superseded. Resolving against the raw array instead would let an
1368
+ * agent forget a predecessor a current successor retired — and forgetting
1369
+ * that successor is documented to release it, which a tombstone on the
1370
+ * predecessor would then silently prevent.
1371
+ */
1372
+ private mutable;
867
1373
  forget(agentId: string, entryId: string): Promise<boolean>;
868
1374
  audit(agentId: string): Promise<MemoryAuditEntry[]>;
869
1375
  reassertTrust(agentId: string, entryId: string, trust: TrustLevel): Promise<boolean>;
1376
+ /**
1377
+ * Allocated over un-tombstoned entries, never the live view: the budget
1378
+ * is a property of the record and must not move when a successor's
1379
+ * window opens or a clock ticks. Sizing it from the live set would free a
1380
+ * superseded pin's bytes, admit a later pin into the space, and make that
1381
+ * later pin inert the moment the successor was forgotten.
1382
+ */
1383
+ private pinBudget;
1384
+ pin(agentId: string, entryId: string): Promise<MemoryPinOutcome>;
1385
+ unpin(agentId: string, entryId: string): Promise<boolean>;
1386
+ pinned(agentId: string, options?: MemoryPinnedOptions): Promise<MemoryEntry[]>;
1387
+ topics(agentId: string): Promise<MemoryTopic[]>;
1388
+ importEntries(agentId: string, entries: readonly MemoryEntry[]): Promise<MemoryImportResult>;
870
1389
  }
871
1390
  /**
872
1391
  * Resolves named credentials for an agent. Implementations must enforce the
@@ -981,10 +1500,51 @@ export declare const uncachedInputTokens: (total: number, ...cached: Array<numbe
981
1500
  */
982
1501
  export declare const totalTokenUsage: (records: readonly TokenUsage[]) => TokenUsage | undefined;
983
1502
  export interface ProviderRequest {
1503
+ /**
1504
+ * The live session. **Persistable**: a provider that saves it, or hands
1505
+ * it to `executeHostedToolCall`, is writing the real conversation — so
1506
+ * this is always the whole session, never a view. Read the transcript
1507
+ * through `transcriptOf(request)` instead of from here.
1508
+ */
984
1509
  session: Session;
1510
+ /**
1511
+ * The transcript to send, when it is not the whole of `session.messages`
1512
+ * — the tail left by the session's context floor, with a note in place
1513
+ * of what was cut.
1514
+ *
1515
+ * Separate from `session` because the two are used for different things
1516
+ * and only one of them may be written back. The harness providers pass
1517
+ * `request.session` to `executeHostedToolCall`, which appends tool
1518
+ * records and saves; the fallback wrapper persists `request.session` to
1519
+ * make its switch durable. Handing either a shortened *session* would
1520
+ * write the window over the stored transcript and lose everything below
1521
+ * the floor, permanently.
1522
+ *
1523
+ * Absent means "all of them", so an adapter that ignores this field
1524
+ * behaves exactly as it did — which is right for a harness provider,
1525
+ * since it manages its own context and the window is not its to apply.
1526
+ */
1527
+ messages?: readonly Message[];
985
1528
  tools?: ToolDescriptor[];
986
- /** Agent-scoped long-term memory, newest last. */
987
- memory?: MemoryEntry[];
1529
+ /**
1530
+ * `none` asks for an answer in words: the tools stay declared, because
1531
+ * the history holds calls to them and an API that is sent tool calls
1532
+ * with no tools to match refuses the request, but the model may not
1533
+ * call one. Sent on the turn that wraps up a message which has used its
1534
+ * `maxTurns` (see `TURN_LIMIT_NOTE`).
1535
+ *
1536
+ * Optional to honour. A provider that ignores it sends a request the
1537
+ * model may still answer with a call, and the runner then fails the turn
1538
+ * the way it did before wrap-up existed, rather than run the call.
1539
+ */
1540
+ toolChoice?: 'none';
1541
+ /**
1542
+ * Agent-scoped long-term memory as the turn injects it: the pinned core,
1543
+ * the topic index, and the recency tail. A bare array is the tail alone —
1544
+ * what `memory` meant before the three blocks existed, and still what a
1545
+ * host building a request by hand may pass.
1546
+ */
1547
+ memory?: MemoryEntry[] | MemoryInjection;
988
1548
  /**
989
1549
  * The skills enabled for this agent — descriptors only, one prompt line
990
1550
  * each. Bodies never travel here; they arrive through `skill.read` when
@@ -1039,6 +1599,16 @@ export interface ProviderRequest {
1039
1599
  */
1040
1600
  signal?: AbortSignal;
1041
1601
  }
1602
+ /**
1603
+ * The transcript a provider should send: the request's window when it has
1604
+ * one, the whole session otherwise.
1605
+ *
1606
+ * One implementation because there is one rule, and because the wrong
1607
+ * answer is silent — an adapter reading `session.messages` directly still
1608
+ * works, it just ignores the floor and sends the request that was already
1609
+ * refused.
1610
+ */
1611
+ export declare const transcriptOf: (request: Pick<ProviderRequest, "session" | "messages">) => readonly Message[];
1042
1612
  export interface ProviderResponse {
1043
1613
  parts: ProviderPart[];
1044
1614
  /**
@@ -1334,6 +1904,22 @@ export interface PromptTextOptions {
1334
1904
  * followed more often than one read an hour ago.
1335
1905
  */
1336
1906
  export declare const UNADDRESSED_TURN_NOTE = "Nobody said that to you, and nobody is waiting on you. Reply only if you have something these people would want from you and do not have \u2014 an answer to a question left open, a correction to something wrong, a thing you were asked to watch for. Otherwise reply with nothing at all: no acknowledgement, no summary, no offer to help. Saying nothing is usually the right answer here.";
1907
+ /**
1908
+ * What the last turn of a message is told, once the message has used every
1909
+ * tool turn `maxTurns` allows it.
1910
+ *
1911
+ * The alternative is what this replaced: the turn failed with "exceeded the
1912
+ * maximum of 8 provider turns", every step it had taken sat in the
1913
+ * transcript unsummarized, and the person got an error for work that had
1914
+ * mostly succeeded. A limit on how far one message may run is a check-in
1915
+ * point, not a failure, so the agent says where it got to and the next
1916
+ * message carries on — the session keeps everything the turns did.
1917
+ *
1918
+ * Sent as the newest message of the request's window, never saved: it is
1919
+ * the runtime speaking for one call, and a transcript that kept it would
1920
+ * read to every later turn as something the person said.
1921
+ */
1922
+ export declare const TURN_LIMIT_NOTE = "You have used every step this message allows, so you cannot call any more tools on it. Reply now, in words: say what you did, what you found, and what is left. If the work is unfinished, end by saying that replying \"continue\" picks it up from here.";
1337
1923
  /** Reads the checkpoint off a session, if it is parked. */
1338
1924
  export declare const readPendingApproval: (session: Session) => PendingApprovalRecord | undefined;
1339
1925
  /**
@@ -1514,6 +2100,23 @@ export type StratusEvent = {
1514
2100
  sessionId: string;
1515
2101
  trust: TrustLevel;
1516
2102
  source: string;
2103
+ }
2104
+ /**
2105
+ * The conversation outgrew the model's context window, so its oldest
2106
+ * messages are no longer sent. `droppedMessages` is what this trim gave
2107
+ * up; `floor` is the total now held back.
2108
+ *
2109
+ * Counts only — no content, for the same reason `session.tainted` names
2110
+ * only its source: this is what the daemon log records, and the log is a
2111
+ * trace, not a second transcript. Worth an event at all because it is
2112
+ * otherwise invisible: the answers stay plausible while the agent
2113
+ * quietly stops being able to remember the start of the conversation.
2114
+ */
2115
+ | {
2116
+ type: 'session.context-trimmed';
2117
+ sessionId: string;
2118
+ droppedMessages: number;
2119
+ floor: number;
1517
2120
  };
1518
2121
  export type EventHandler = (event: StratusEvent) => void | Promise<void>;
1519
2122
  export interface EventBusOptions {
@@ -1652,9 +2255,35 @@ export interface ChannelContribution {
1652
2255
  * memories, the way a `tool-fs` that cached its roots would hand every
1653
2256
  * agent the first agent's files.
1654
2257
  */
2258
+ /**
2259
+ * The revision of `AgentMemoryStore` a contributed store is built against.
2260
+ *
2261
+ * Bumped when the contract changes shape in a way a plugin compiled against
2262
+ * the old one cannot satisfy — which it *silently* cannot, because the
2263
+ * change that matters here is the shape of an argument rather than the
2264
+ * number of them. A v1 store's `search(agentId, query, limit)` still
2265
+ * accepts three arguments and still returns entries when the host hands it
2266
+ * an options object; it just binds `{ limit: 10 }` where a number belonged,
2267
+ * and answers with an unbounded read or none at all. No runtime check can
2268
+ * tell the two apart from the function, so the plugin declares it.
2269
+ *
2270
+ * - **1** — `append(agentId, content, metadata?, provenance?)` and
2271
+ * `search(agentId, query, limit?)`, the shape before 29.
2272
+ * - **2** — `append(agentId, content, options?)` and
2273
+ * `search(agentId, query, options?)`, plus the optional `pin`, `unpin`,
2274
+ * `pinned`, `topics`, and `importEntries`.
2275
+ */
2276
+ export declare const MEMORY_STORE_CONTRACT_VERSION = 2;
1655
2277
  export interface MemoryStoreContribution {
1656
2278
  name: string;
1657
2279
  store: AgentMemoryStore;
2280
+ /**
2281
+ * Must be `MEMORY_STORE_CONTRACT_VERSION`. A store declaring an older
2282
+ * revision — or none, which is what a plugin built before this field
2283
+ * existed produces — is refused at registration rather than routed
2284
+ * traffic it will mishandle without saying so.
2285
+ */
2286
+ contract: typeof MEMORY_STORE_CONTRACT_VERSION;
1658
2287
  }
1659
2288
  /** An executor contributed by a plugin, selectable by name in a trusted config. */
1660
2289
  export interface ExecutorContribution {
@@ -1709,8 +2338,12 @@ export declare class ChannelRegistry {
1709
2338
  * memory tools are built before any plugin has loaded, so the store they
1710
2339
  * hold has to be one that can change its mind — and an agent-by-agent
1711
2340
  * answer is what keeps a contributed store from being process-global.
1712
- * `reassertTrust` is forwarded only when the chosen store has it: a store
1713
- * that omitted it cannot be re-asserted, and this must not claim otherwise.
2341
+ * Every optional method is forwarded only when the chosen store has it: a
2342
+ * store that omitted one cannot do that thing, and this must not claim
2343
+ * otherwise. The routed store declares them all, because which store
2344
+ * serves an agent is not known until the call — so the honest answer for a
2345
+ * store that lacks one is the same answer the seam's own default gives
2346
+ * (nothing pinned, no topics, no re-assertion).
1714
2347
  */
1715
2348
  export declare const createRoutedMemoryStore: (storeFor: (agentId: string) => AgentMemoryStore) => AgentMemoryStore;
1716
2349
  /** The handle a plugin registers providers through. Manifest-bound; see `@stratusagent/plugins`. */
@@ -1737,6 +2370,57 @@ export interface ChannelRegistrationHandle {
1737
2370
  */
1738
2371
  transportSecrets(kind: string): Promise<ChannelTransportSecrets>;
1739
2372
  }
2373
+ /**
2374
+ * Where each agent's files go, answered by the host.
2375
+ *
2376
+ * A seam rather than a path in a config block because the layout is this
2377
+ * repository's to own: the workspace is `~/.stratus/agents/<id>/workspace`
2378
+ * today and was `~/.stratus/workspaces/<id>` before it, and every plugin
2379
+ * that joined the agent id onto a root held a copy of that shape. Five of
2380
+ * them did, and all five had to change when it moved — which is the cost
2381
+ * the `workspaceRoot` key was introduced to avoid and did not.
2382
+ *
2383
+ * Asked per call, from `session.agent.id`, for the same reason credentials
2384
+ * are: the answer is the *calling* agent's, and one captured at setup is
2385
+ * the first agent's forever.
2386
+ */
2387
+ export interface AgentWorkspaces {
2388
+ /**
2389
+ * Where this agent's files go. Resolution only: nothing is created and
2390
+ * nothing can fail that would not fail for a bad id, so a caller that
2391
+ * merely needs to *name* the path — the provenance ledger looking for a
2392
+ * file that may not exist — can ask without consequence.
2393
+ */
2394
+ forAgent(agentId: string): string;
2395
+ /**
2396
+ * The same path, created and with its permissions settled, for a caller
2397
+ * about to write.
2398
+ *
2399
+ * The host does this rather than the plugin because the workspace sits
2400
+ * inside the agent's state directory, beside its sessions, memories and
2401
+ * grants — so the recursive `mkdir` a file-producing plugin would
2402
+ * otherwise reach for makes a *state* directory under the process umask.
2403
+ * A plugin has no way to know that; the host that chose the layout does.
2404
+ * Callers may still create subdirectories under what they are handed.
2405
+ *
2406
+ * Separate from {@link forAgent} because it can fail, and what fails
2407
+ * with it should be only what needed the directory. A read that wanted
2408
+ * a path, a navigation that writes nothing, a tool result that turned
2409
+ * out to be text — none of those should stop because a directory they
2410
+ * were never going to use could not be made.
2411
+ */
2412
+ prepare(agentId: string): string;
2413
+ /**
2414
+ * Every agent workspace the host can currently name, for the guards that
2415
+ * have to recognise *any* agent's file rather than the caller's — the
2416
+ * filesystem provenance ledger is the one that does.
2417
+ *
2418
+ * Paths, not a promise that each exists: a caller canonicalizes and
2419
+ * tolerates what is missing. Re-read rather than cached, so an agent
2420
+ * added under a running daemon is visible to the next call.
2421
+ */
2422
+ all(): Promise<readonly string[]>;
2423
+ }
1740
2424
  export interface MemoryRegistrationHandle {
1741
2425
  register(contribution: MemoryStoreContribution): void;
1742
2426
  }
@@ -1774,6 +2458,19 @@ export interface PluginContext {
1774
2458
  channels?: ChannelRegistrationHandle;
1775
2459
  memory?: MemoryRegistrationHandle;
1776
2460
  executors?: ExecutorRegistrationHandle;
2461
+ /**
2462
+ * Where this plugin may write files, per agent. See {@link AgentWorkspaces}.
2463
+ *
2464
+ * A host that omits it leaves every plugin that produces a file with
2465
+ * nowhere the host has chosen to put it. Such a plugin falls back to the
2466
+ * `workspaceRoot` config key — the documented root it joins the agent id
2467
+ * onto — and, given neither, must fail the call naming what is missing,
2468
+ * the way `browser.screenshot` does. Never a directory of its own
2469
+ * choosing: a file an agent was told it produced has to be somewhere the
2470
+ * operator can find, and a turn that cannot store its output must not
2471
+ * read as though it did.
2472
+ */
2473
+ workspaces?: AgentWorkspaces;
1777
2474
  /**
1778
2475
  * The host's log, for what a plugin has to say after `setup` returns —
1779
2476
  * a server that dropped, a reconnect that failed. The daemon's is the
@@ -1986,17 +2683,6 @@ export declare const renderPersonaSection: (agent: Pick<AgentDefinition, "name"
1986
2683
  * reads a stranger's sentence as a stranger's.
1987
2684
  */
1988
2685
  export declare const memoryRegionHeading: (trust: TrustLevel) => string;
1989
- /**
1990
- * The memory block, one region per trust label present, most trusted
1991
- * first, entries in the order given within each.
1992
- *
1993
- * The invariant this enforces: nothing in the prompt derived from an entry
1994
- * renders under a higher label than that entry carries. Stated over the
1995
- * whole ordering rather than over `external` alone, because `unknown`
1996
- * stopped being only a legacy artifact the moment an unauthorized sender's
1997
- * message started producing it. An entry with no label renders in the
1998
- * `unknown` region — never as the agent's own conclusion.
1999
- */
2000
2686
  /**
2001
2687
  * Unicode's `Bidi_Control` property as a character-class fragment: the
2002
2688
  * marks, the Arabic letter mark, the embeddings and overrides, and the
@@ -2017,7 +2703,23 @@ export declare const BIDI_CONTROL_CHARACTERS = "\\u061c\\u200e\\u200f\\u202a-\\u
2017
2703
  * shown is still what is stored.
2018
2704
  */
2019
2705
  export declare const escapeControlCharacters: (text: string) => string;
2020
- export declare const renderMemorySection: (memory: readonly MemoryEntry[] | undefined) => string | undefined;
2706
+ /**
2707
+ * The memory block: the pinned core, the topic index, and the recency tail,
2708
+ * each grouped into trust regions, joined into **one** section.
2709
+ *
2710
+ * One section rather than three is a requirement rather than a formatting
2711
+ * preference. An adapter that places the volatile section separately finds
2712
+ * it with `parts.find((part) => part.kind === 'memory')` and strips it with
2713
+ * the matching filter, so three sections sharing the kind would send the
2714
+ * first to the tail and drop the other two from the request entirely —
2715
+ * and three new kinds would leave them on the cached prefix, which is
2716
+ * exactly what memory was moved off.
2717
+ *
2718
+ * A bare array is accepted and read as the recency tail alone: that is what
2719
+ * `memory` meant before the three blocks existed, and a provider test or a
2720
+ * host building a request by hand should not have to know about pinning.
2721
+ */
2722
+ export declare const renderMemorySection: (memory: readonly MemoryEntry[] | MemoryInjection | undefined) => string | undefined;
2021
2723
  /**
2022
2724
  * The skills block: one line per enabled skill — id, name, and the
2023
2725
  * description routing runs on. This is the whole point of the step: the
@@ -2025,6 +2727,18 @@ export declare const renderMemorySection: (memory: readonly MemoryEntry[] | unde
2025
2727
  * here may ever include a body.
2026
2728
  */
2027
2729
  export declare const renderSkillsSection: (skills: readonly SkillDescriptor[] | undefined) => string | undefined;
2730
+ /**
2731
+ * Where the conversation is happening, for a session a channel started.
2732
+ *
2733
+ * A channel adapter records itself as `metadata.channel` on the sessions it
2734
+ * dispatches (`slack` today). Without this the agent could not tell: an
2735
+ * agent with no Slack tool, asked about an attachment in a Slack DM, told
2736
+ * 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
2739
+ * id, since it is interpolated into the prompt.
2740
+ */
2741
+ export declare const renderChannelSection: (session: Pick<Session, "metadata">) => string | undefined;
2028
2742
  export interface SystemPromptOptions {
2029
2743
  /** Host-level preamble, rendered before the agent's own persona. */
2030
2744
  preamble?: string;
@@ -2035,12 +2749,12 @@ export interface SystemPromptOptions {
2035
2749
  * Which part of what an agent is told a section is.
2036
2750
  *
2037
2751
  * The distinction a caller actually needs is stable versus volatile:
2038
- * `preamble`, `persona`, and `skills` are byte-identical across every turn of
2752
+ * `preamble`, `replies`, `persona`, `channel`, and `skills` are byte-identical across every turn of
2039
2753
  * an agent's life, while `memory` is rewritten whenever the agent remembers
2040
2754
  * anything. A provider that caches its request prefix has to place those two
2041
2755
  * groups differently, and it cannot tell them apart from rendered strings.
2042
2756
  */
2043
- export type SystemPromptSectionKind = 'preamble' | 'persona' | 'memory' | 'skills';
2757
+ export type SystemPromptSectionKind = 'preamble' | 'replies' | 'persona' | 'channel' | 'memory' | 'skills';
2044
2758
  export interface SystemPromptSection {
2045
2759
  kind: SystemPromptSectionKind;
2046
2760
  text: string;
@@ -2209,6 +2923,78 @@ export interface ObserveInput {
2209
2923
  */
2210
2924
  metadata?: JsonObject;
2211
2925
  }
2926
+ /**
2927
+ * Thrown by a provider when the request did not fit the model's context
2928
+ * window — the transcript, not the answer, is what was too long.
2929
+ *
2930
+ * A distinct type because it is the one provider failure the kernel can do
2931
+ * something about. Everything else a provider throws is final; this one
2932
+ * says "the same request will never work, a shorter one might", and the
2933
+ * runner answers by sending less history (see `raiseContextFloor`).
2934
+ *
2935
+ * Providers opt in. Recognising it means matching a vendor's own wording
2936
+ * for a 400, which only the adapter can do — and a harness provider that
2937
+ * manages its own context (`provider-claude-code`, `provider-codex`) has
2938
+ * nothing to report, because the transcript it sends is not ours to
2939
+ * shorten. A provider that never throws this behaves exactly as it did.
2940
+ */
2941
+ export declare class ContextOverflowError extends Error {
2942
+ constructor(message: string);
2943
+ }
2944
+ /**
2945
+ * How many of a session's oldest messages are no longer sent to the
2946
+ * provider.
2947
+ *
2948
+ * Durable, and **monotonic** like the trust label: a transcript only grows,
2949
+ * so a floor that has had to rise once would have to rise again on the next
2950
+ * turn, and re-discovering that costs a rejected request every time. Raised
2951
+ * only by `raiseContextFloor`, never lowered — a conversation does not get
2952
+ * shorter.
2953
+ *
2954
+ * It bounds what is *sent*, never what is stored. The transcript is the
2955
+ * record of what happened and stays whole on disk; this is a window onto
2956
+ * its tail. `stratus session rollover` is still the way to actually leave a
2957
+ * conversation behind.
2958
+ */
2959
+ export declare const CONTEXT_FLOOR_METADATA_KEY = "contextFloor";
2960
+ /** The stored floor, or 0 for a session that has never overflowed. */
2961
+ export declare const contextFloorOf: (session: Pick<Session, "metadata">) => number;
2962
+ /**
2963
+ * The note that stands in for the messages a floor cut away, prefixed to
2964
+ * the oldest message the model still sees.
2965
+ *
2966
+ * Said rather than done silently, for the reason every other cut in this
2967
+ * codebase is announced: a model that cannot tell a conversation that
2968
+ * started here from one that was trimmed will answer as though the earlier
2969
+ * turns never happened, and confidently.
2970
+ */
2971
+ export declare const droppedTurnsNote: (count: number) => string;
2972
+ /**
2973
+ * The messages above `session`'s floor, with the
2974
+ * note in place of what was cut. The session itself is untouched — the
2975
+ * copy is shallow and deliberately shares `metadata`, because providers
2976
+ * keep replay state there (the Anthropic raw-turn cache) and a turn that
2977
+ * wrote into a copy would lose it.
2978
+ *
2979
+ * Returns the session unchanged when the floor is 0, so a conversation that
2980
+ * has never overflowed is not copied on every turn.
2981
+ */
2982
+ export declare const messagesWithinContextFloor: (session: Session) => readonly Message[];
2983
+ /**
2984
+ * Raise the floor so the next attempt sends less, and report whether there
2985
+ * was anything left to give up.
2986
+ *
2987
+ * Halves the remaining window each time rather than trimming a fixed
2988
+ * amount: nothing here knows how many tokens a message is, and an overflow
2989
+ * says only "too big", not "too big by this much" — so the cheap thing is
2990
+ * to converge in a handful of attempts rather than to creep towards the
2991
+ * answer one turn at a time, paying a rejected request for each step.
2992
+ *
2993
+ * Returns false when the window is already down to the newest turn. That is
2994
+ * the floor's floor: a single turn that does not fit is a message larger
2995
+ * than the model can read, and no amount of dropping history fixes it.
2996
+ */
2997
+ export declare const raiseContextFloor: (session: Session) => boolean;
2212
2998
  /**
2213
2999
  * Thrown when a run is stopped by its abort signal. The session ends up
2214
3000
  * `failed` with this error's message as `lastError`, so an aborted turn is
@@ -2272,6 +3058,18 @@ export interface AgentRunnerOptions {
2272
3058
  */
2273
3059
  streaming?: boolean;
2274
3060
  }
3061
+ /**
3062
+ * Tool turns one message may take before it wraps up. The harness providers
3063
+ * spend the same number as their own inner budget, so they import it rather
3064
+ * than keep a copy.
3065
+ *
3066
+ * 8 until the wrap-up turn existed, and 8 was the whole budget for one Slack
3067
+ * message: an agent with a shell spent it on a handful of commands and
3068
+ * failed with nothing said. With a wrap-up turn the ceiling is where a long
3069
+ * task checks in rather than where it dies, and 40 is enough for real
3070
+ * multi-step work while still bounding what a runaway loop can spend.
3071
+ */
3072
+ export declare const DEFAULT_MAX_TURNS = 40;
2275
3073
  export declare class AgentRunner {
2276
3074
  readonly bus: EventBus;
2277
3075
  readonly store: SessionStore;