@stratusagent/core 0.11.3 → 0.11.5

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
@@ -204,6 +204,16 @@ export interface AgentDefinition extends AgentDescriptor {
204
204
  skills?: string[];
205
205
  /** Credential names this agent may resolve. Omitted = none. */
206
206
  credentials?: string[];
207
+ /**
208
+ * Agent ids this agent may delegate to through `agent.delegate`, or `*`
209
+ * for any agent on the roster. Omitted = none, matching `credentials`
210
+ * rather than `tools`: delegation runs a turn as another agent, under
211
+ * that agent's tools, credentials, and memory, so it is the lateral
212
+ * move a prompt-injected agent would take — and `agent.delegate` is
213
+ * `safe`, so nothing else asks. Checked by the delegate tool against the
214
+ * session's agent, the way the tool allowlist is.
215
+ */
216
+ delegates?: string[];
207
217
  /**
208
218
  * How this agent listens in a conversation it shares with people talking
209
219
  * to each other — see `ListensMode`. Omitted = `thread`. Part of what an
@@ -232,6 +242,14 @@ export declare const LISTENS_MODES: readonly ListensMode[];
232
242
  export declare const isListensMode: (value: unknown) => value is ListensMode;
233
243
  export declare class AgentRegistry {
234
244
  private agents;
245
+ /**
246
+ * `*` in a `delegates` list means any agent on the roster, so an agent
247
+ * with that id could never be granted alone — `delegates: ['*']` would
248
+ * open the whole roster to reach it. The soul loader refuses the id;
249
+ * this refuses it for a definition built in code, since the wildcard's
250
+ * meaning is decided here, where the roster lives, and not by which
251
+ * path put the agent on it.
252
+ */
235
253
  register(agent: AgentDefinition): AgentDefinition;
236
254
  get(id: string): AgentDefinition | undefined;
237
255
  /**
@@ -654,15 +672,75 @@ export interface ToolDescriptor {
654
672
  /** Carried so providers hosting their own loop can see it too. */
655
673
  risk?: ToolRisk;
656
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;
657
689
  /**
658
690
  * One remembered fact. Memory is keyed by agent, never by session or
659
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.
660
698
  */
661
699
  export interface MemoryEntry {
662
700
  id: string;
663
701
  agentId: string;
664
702
  content: string;
665
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;
666
744
  metadata?: JsonObject;
667
745
  /**
668
746
  * Where the fact came from — the least trusted thing the writing session
@@ -681,6 +759,21 @@ export interface MemoryEntry {
681
759
  * reading the store, and travels with the entry as ordinary data.
682
760
  */
683
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;
684
777
  }
685
778
  export interface MemoryOrigin {
686
779
  sessionId?: string;
@@ -719,16 +812,60 @@ export declare const MEMORY_ENTRY_MAX_BYTES = 4096;
719
812
  * several cap-size entries still fit (see `MEMORY_ENTRY_MAX_BYTES`).
720
813
  */
721
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";
722
822
  /** Results a `search` returns when the caller names no limit. */
723
823
  export declare const MEMORY_RECALL_DEFAULT_LIMIT = 10;
724
824
  /** Ceiling a `search` limit is clamped to, whatever the caller asked for. */
725
825
  export declare const MEMORY_RECALL_MAX_LIMIT = 50;
726
- /** How many recent entries the runner injects into the system prompt. */
727
- 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;
728
849
  /** UTF-8 size of an entry's content — the unit both memory caps are stated in. */
729
850
  export declare const memoryContentByteLength: (content: string) => number;
730
851
  /** Refuses (never truncates) content over the per-entry cap: a half-stored fact is worse than a refused one. */
731
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;
732
869
  /**
733
870
  * The tokenizer both store implementations share: NFC-normalized,
734
871
  * case-folded, split on anything that is not a Unicode letter or digit.
@@ -738,12 +875,72 @@ export declare const assertMemoryContentWithinCap: (content: string) => void;
738
875
  */
739
876
  export declare const tokenizeMemoryText: (text: string) => string[];
740
877
  /**
741
- * Whether content matches a tokenized query: every query token must be
742
- * present as a whole token. A query that tokenized to nothing matches
743
- * nothing — recall with no searchable terms is a question that was not
744
- * 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.
904
+ */
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.
745
942
  */
746
- export declare const memoryQueryMatches: (content: string, queryTokens: readonly string[]) => boolean;
943
+ export declare const supersededMemoryIdsAt: (entries: readonly MemoryEntry[], at: Date) => Set<string>;
747
944
  /**
748
945
  * Recall order — newest first, ties on `createdAt` broken by entry id
749
946
  * ascending. The tie-break is not about frequency: without one, two
@@ -754,10 +951,30 @@ export declare const memoryQueryMatches: (content: string, queryTokens: readonly
754
951
  export declare const compareMemoryRecallOrder: (a: MemoryEntry, b: MemoryEntry) => number;
755
952
  /** Chronological order (oldest first), same tie-break — how a bounded `list` presents its slice. */
756
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;
757
968
  export interface MemoryReadResult {
758
969
  entries: MemoryEntry[];
759
970
  /** True when live entries beyond these existed — the entry limit or the byte budget bound, whichever bit first. */
760
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;
761
978
  }
762
979
  export interface MemoryListOptions {
763
980
  /**
@@ -768,6 +985,53 @@ export interface MemoryListOptions {
768
985
  * feed a model always pass a bound.
769
986
  */
770
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;
771
1035
  }
772
1036
  /** Clamp a caller-chosen search limit into the store's own bounds. */
773
1037
  export declare const clampMemoryRecallLimit: (limit?: number) => number;
@@ -785,7 +1049,130 @@ export declare const boundMemoryRead: (candidates: readonly MemoryEntry[], limit
785
1049
  * presentation order are different things; conflating them is the bug this
786
1050
  * helper exists so nobody writes three times.
787
1051
  */
788
- 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[];
789
1176
  /**
790
1177
  * Durable, searchable memory an agent writes and reads deliberately. The
791
1178
  * per-agent key is the access boundary — every method takes the agent id the
@@ -805,19 +1192,30 @@ export declare const boundMemoryList: (candidates: readonly MemoryEntry[], limit
805
1192
  * `MEMORY_READ_MAX_BYTES` when a limit is in play; `forget` tombstones
806
1193
  * rather than deletes, and `audit` is where tombstoned entries remain
807
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`.
808
1200
  */
809
1201
  export interface AgentMemoryStore {
810
1202
  /**
811
1203
  * Refuses content over `MEMORY_ENTRY_MAX_BYTES` rather than truncating it.
812
- * `provenance` is what the writer knows about where the fact came from;
813
- * an append without one stores an entry that reads as `unknown`, which
814
- * 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.
815
1208
  */
816
- append(agentId: string, content: string, metadata?: JsonObject, provenance?: MemoryProvenance): Promise<MemoryEntry>;
817
- /** 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. */
818
1211
  list(agentId: string, options?: MemoryListOptions): Promise<MemoryReadResult>;
819
- /** Live entries matching the literal query, newest first, bounded. */
820
- 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>;
821
1219
  /** Tombstones a live entry. False when no live entry of this agent has that id. */
822
1220
  forget(agentId: string, entryId: string): Promise<boolean>;
823
1221
  /** The operator's audit read: every entry, tombstoned included, oldest first. */
@@ -834,21 +1232,160 @@ export interface AgentMemoryStore {
834
1232
  * operator has no way out of `unknown` but rewriting the record.
835
1233
  */
836
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>;
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[];
837
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;
838
1330
  export declare class InMemoryAgentMemoryStore implements AgentMemoryStore {
839
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;
840
1353
  private counter;
841
1354
  private readonly now;
842
1355
  constructor(options?: {
843
1356
  now?: () => Date;
844
1357
  });
845
- 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`. */
846
1360
  private live;
847
1361
  list(agentId: string, options?: MemoryListOptions): Promise<MemoryReadResult>;
848
- 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;
849
1373
  forget(agentId: string, entryId: string): Promise<boolean>;
850
1374
  audit(agentId: string): Promise<MemoryAuditEntry[]>;
851
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>;
852
1389
  }
853
1390
  /**
854
1391
  * Resolves named credentials for an agent. Implementations must enforce the
@@ -963,10 +1500,51 @@ export declare const uncachedInputTokens: (total: number, ...cached: Array<numbe
963
1500
  */
964
1501
  export declare const totalTokenUsage: (records: readonly TokenUsage[]) => TokenUsage | undefined;
965
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
+ */
966
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[];
967
1528
  tools?: ToolDescriptor[];
968
- /** Agent-scoped long-term memory, newest last. */
969
- 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;
970
1548
  /**
971
1549
  * The skills enabled for this agent — descriptors only, one prompt line
972
1550
  * each. Bodies never travel here; they arrive through `skill.read` when
@@ -1021,6 +1599,16 @@ export interface ProviderRequest {
1021
1599
  */
1022
1600
  signal?: AbortSignal;
1023
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[];
1024
1612
  export interface ProviderResponse {
1025
1613
  parts: ProviderPart[];
1026
1614
  /**
@@ -1316,6 +1904,22 @@ export interface PromptTextOptions {
1316
1904
  * followed more often than one read an hour ago.
1317
1905
  */
1318
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.";
1319
1923
  /** Reads the checkpoint off a session, if it is parked. */
1320
1924
  export declare const readPendingApproval: (session: Session) => PendingApprovalRecord | undefined;
1321
1925
  /**
@@ -1496,6 +2100,23 @@ export type StratusEvent = {
1496
2100
  sessionId: string;
1497
2101
  trust: TrustLevel;
1498
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;
1499
2120
  };
1500
2121
  export type EventHandler = (event: StratusEvent) => void | Promise<void>;
1501
2122
  export interface EventBusOptions {
@@ -1540,6 +2161,272 @@ export declare class InMemorySessionStore implements SessionStore {
1540
2161
  save(session: Session): Promise<void>;
1541
2162
  listIdsByStatus(status: SessionStatus): Promise<string[]>;
1542
2163
  }
2164
+ /**
2165
+ * The providers the host builds itself. Here, in the dependency-free
2166
+ * package, because two packages that never import each other need the one
2167
+ * list: `@stratusagent/state` resolves a selection against it, and
2168
+ * `@stratusagent/plugins` refuses a manifest that declares one of these as
2169
+ * a provider of its own — `provider: anthropic` in a soul has to keep
2170
+ * meaning the first-party adapter, and a plugin that could shadow it would
2171
+ * be a plugin that could receive every key the operator stored for it.
2172
+ */
2173
+ export declare const BUILTIN_PROVIDER_NAMES: readonly ["demo", "openai", "anthropic", "codex"];
2174
+ export type BuiltinProviderName = (typeof BUILTIN_PROVIDER_NAMES)[number];
2175
+ /**
2176
+ * The names a trusted config's `executor` and `memoryStore` keys mean the
2177
+ * built-ins by — the local child-process executor and the file store —
2178
+ * and so the names a plugin may not register: the host's selection
2179
+ * special-cases them and would never look a contribution up under them.
2180
+ * Here for the same reason the provider list is: the manifest parser and
2181
+ * the selection logic live in packages that never import each other.
2182
+ */
2183
+ export declare const BUILTIN_EXECUTOR_NAME = "local";
2184
+ export declare const BUILTIN_MEMORY_STORE_NAME = "file";
2185
+ /**
2186
+ * What a soul's `provider:` and `model:` pick, handed to a contributed
2187
+ * provider so it can build the right client. `systemPrompt` is the
2188
+ * operator's preamble from the config file — the persona itself arrives on
2189
+ * every request as `session.agent`, so a provider that ignores this still
2190
+ * speaks as the agent; what it loses is the daemon-wide preamble.
2191
+ */
2192
+ export interface ProviderSelection {
2193
+ model?: string;
2194
+ systemPrompt?: string;
2195
+ }
2196
+ /**
2197
+ * A provider contributed by a plugin.
2198
+ *
2199
+ * A factory rather than a `ModelProvider`, and the reason is the soul: a
2200
+ * provider is built for a model, and `model:` in one soul and another in
2201
+ * the next have to reach it. A registered instance would serve one model
2202
+ * for the whole fleet. The host calls `create` once per distinct selection
2203
+ * and keeps the result, so a plugin holding a client per model holds one
2204
+ * per model, never one per turn.
2205
+ */
2206
+ export interface ProviderContribution {
2207
+ /** The name a soul's `provider:` selects — `ollama`, never a package name. */
2208
+ name: string;
2209
+ create(selection: ProviderSelection): ModelProvider;
2210
+ /**
2211
+ * Whether providers built here report progress through `onDelta`. A
2212
+ * host arms its stall watchdog only for a streaming provider — silence
2213
+ * from one means a stall, while a non-streaming call is silent until it
2214
+ * answers, and a watchdog armed over it would abort every long turn.
2215
+ * Default false, the safe reading for a provider that did not say.
2216
+ */
2217
+ streams?: boolean;
2218
+ }
2219
+ /**
2220
+ * The slice of a channel adapter the kernel holds: structurally
2221
+ * `@stratusagent/channels`' `ChannelAdapter`, kept structural here so core
2222
+ * carries no dependency on the channels package — the same reason the
2223
+ * gateway declares its own copy. `name` is the channel kind (`slack`,
2224
+ * `discord`); the gateway it is started with is the host's.
2225
+ */
2226
+ export interface ChannelAdapterLike {
2227
+ name: string;
2228
+ start(gateway: unknown): Promise<void>;
2229
+ stop(): Promise<void>;
2230
+ required?: boolean;
2231
+ resolveOutbound?(address: {
2232
+ agentId: string;
2233
+ to: string;
2234
+ }): Promise<{
2235
+ post(text: string): Promise<unknown>;
2236
+ }>;
2237
+ }
2238
+ /**
2239
+ * A channel contributed by a plugin: the adapter plus the agents it
2240
+ * carries. Channels key on (agent, kind), not on the agent alone — one
2241
+ * agent reachable on Slack and on Discord is two registrations that do
2242
+ * not collide; two adapters both claiming Discord for that agent do.
2243
+ */
2244
+ export interface ChannelContribution {
2245
+ /** Its `name` is the channel kind. */
2246
+ adapter: ChannelAdapterLike;
2247
+ /** Agent ids this adapter carries: the (agent, kind) pairs it claims. */
2248
+ agents: readonly string[];
2249
+ }
2250
+ /**
2251
+ * A memory store contributed by a plugin. Every `AgentMemoryStore` method
2252
+ * takes the agent id, and that is what makes a contributed store
2253
+ * per-agent-resolvable rather than process-global: a plugin that closed
2254
+ * over one agent at setup would hand every agent the first agent's
2255
+ * memories, the way a `tool-fs` that cached its roots would hand every
2256
+ * agent the first agent's files.
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;
2277
+ export interface MemoryStoreContribution {
2278
+ name: string;
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;
2287
+ }
2288
+ /** An executor contributed by a plugin, selectable by name in a trusted config. */
2289
+ export interface ExecutorContribution {
2290
+ name: string;
2291
+ executor: Executor;
2292
+ }
2293
+ /**
2294
+ * A name-keyed registry for one contribution kind — the counterpart of
2295
+ * `ToolRegistry` for providers, memory stores, and executors. Like it, a
2296
+ * bare `Map.set`: it keeps no provenance and refuses nothing, and a plugin
2297
+ * never touches one directly. The manifest-bound view in
2298
+ * `@stratusagent/plugins` is what refuses an undeclared name or a
2299
+ * collision; this is where a whole plugin's contributions land once it has
2300
+ * loaded.
2301
+ */
2302
+ export declare class ContributionRegistry<T> {
2303
+ private readonly entries;
2304
+ register(name: string, value: T): void;
2305
+ get(name: string): T | undefined;
2306
+ has(name: string): boolean;
2307
+ names(): string[];
2308
+ list(): T[];
2309
+ }
2310
+ /** The (agent, kind) key a channel registration claims. */
2311
+ export declare const channelClaimKey: (kind: string, agentId: string) => string;
2312
+ /**
2313
+ * The registry for contributed channels. Keyed by (agent, kind) rather than
2314
+ * by name, because that is what collides — see `ChannelContribution` — and
2315
+ * listed as adapters, because that is what a host starts. Same discipline
2316
+ * as the other registries: no refusal here, the view refuses.
2317
+ */
2318
+ export declare class ChannelRegistry {
2319
+ private readonly contributions;
2320
+ /** Undefined marks a claim the host made for an adapter it wired itself. */
2321
+ private readonly claims;
2322
+ register(contribution: ChannelContribution): void;
2323
+ /**
2324
+ * Claim (agent, kind) pairs the host carries itself, so a plugin
2325
+ * registering the same kind for the same agent collides with the host's
2326
+ * adapter exactly as it would with another plugin's — two adapters on
2327
+ * one agent's Slack would each answer every message.
2328
+ */
2329
+ claim(kind: string, agentIds: readonly string[]): void;
2330
+ /** Whether some adapter — the host's or a plugin's — already carries this agent on this kind. */
2331
+ claimed(kind: string, agentId: string): boolean;
2332
+ list(): ChannelContribution[];
2333
+ }
2334
+ /**
2335
+ * A memory store that resolves which store serves an agent **per call**.
2336
+ *
2337
+ * The seam a host selects a contributed store through. The runner and the
2338
+ * memory tools are built before any plugin has loaded, so the store they
2339
+ * hold has to be one that can change its mind — and an agent-by-agent
2340
+ * answer is what keeps a contributed store from being process-global.
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).
2347
+ */
2348
+ export declare const createRoutedMemoryStore: (storeFor: (agentId: string) => AgentMemoryStore) => AgentMemoryStore;
2349
+ /** The handle a plugin registers providers through. Manifest-bound; see `@stratusagent/plugins`. */
2350
+ export interface ProviderRegistrationHandle {
2351
+ register(contribution: ProviderContribution): void;
2352
+ }
2353
+ /**
2354
+ * The transport secrets a channel plugin receives, by agent id: the values
2355
+ * under `channels.<kind>.<agentId>` in the host's credential store. Every
2356
+ * value is a string — a token, an app id — and nothing here is ever
2357
+ * resolved through an agent's own credential allowlist.
2358
+ */
2359
+ export type ChannelTransportSecrets = Record<string, Record<string, string>>;
2360
+ export interface ChannelRegistrationHandle {
2361
+ register(contribution: ChannelContribution): void;
2362
+ /**
2363
+ * The host-owned path for a channel's transport secrets, for a kind
2364
+ * this plugin's manifest declares. Host-owned because the invariant is
2365
+ * firm: a channel token is a gateway infrastructure secret, never
2366
+ * resolvable through the agent-scoped `credentials` resolver, since an
2367
+ * agent must not read the tokens of the transport carrying it. Without
2368
+ * this a channel plugin could not initialize at all — it has no other
2369
+ * defined way to learn which agents it carries.
2370
+ */
2371
+ transportSecrets(kind: string): Promise<ChannelTransportSecrets>;
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
+ }
2424
+ export interface MemoryRegistrationHandle {
2425
+ register(contribution: MemoryStoreContribution): void;
2426
+ }
2427
+ export interface ExecutorRegistrationHandle {
2428
+ register(contribution: ExecutorContribution): void;
2429
+ }
1543
2430
  export interface PluginContext {
1544
2431
  bus: EventBus;
1545
2432
  tools: ToolRegistry;
@@ -1557,6 +2444,33 @@ export interface PluginContext {
1557
2444
  * plugin is code, so scoping it is an interface rather than a boundary.
1558
2445
  */
1559
2446
  credentials?: CredentialResolver;
2447
+ /**
2448
+ * The registration handles for the four contribution kinds that are not
2449
+ * tools: each a manifest-bound view, never the raw registry, following
2450
+ * the shape `tools` established. Optional because a bare
2451
+ * `PluginRegistry.loadAll({ bus, tools })` host supplies none; the
2452
+ * `@stratusagent/plugins` loader supplies all four. A plugin that needs
2453
+ * one and finds it absent must fail its `setup` naming the kind, so the
2454
+ * operator learns the host cannot carry it — never register nothing and
2455
+ * report itself loaded.
2456
+ */
2457
+ providers?: ProviderRegistrationHandle;
2458
+ channels?: ChannelRegistrationHandle;
2459
+ memory?: MemoryRegistrationHandle;
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;
1560
2474
  /**
1561
2475
  * The host's log, for what a plugin has to say after `setup` returns —
1562
2476
  * a server that dropped, a reconnect that failed. The daemon's is the
@@ -1770,27 +2684,42 @@ export declare const renderPersonaSection: (agent: Pick<AgentDefinition, "name"
1770
2684
  */
1771
2685
  export declare const memoryRegionHeading: (trust: TrustLevel) => string;
1772
2686
  /**
1773
- * The memory block, one region per trust label present, most trusted
1774
- * first, entries in the order given within each.
1775
- *
1776
- * The invariant this enforces: nothing in the prompt derived from an entry
1777
- * renders under a higher label than that entry carries. Stated over the
1778
- * whole ordering rather than over `external` alone, because `unknown`
1779
- * stopped being only a legacy artifact the moment an unauthorized sender's
1780
- * message started producing it. An entry with no label renders in the
1781
- * `unknown` region — never as the agent's own conclusion.
2687
+ * Unicode's `Bidi_Control` property as a character-class fragment: the
2688
+ * marks, the Arabic letter mark, the embeddings and overrides, and the
2689
+ * isolates. Exported so every escaper spells out the same set — text that
2690
+ * reads one way to a person and another to the model is the same trick
2691
+ * whether it arrives in a memory entry, a Slack name, or a tool description.
1782
2692
  */
2693
+ export declare const BIDI_CONTROL_CHARACTERS = "\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
1783
2694
  /**
1784
2695
  * `text` on one line, every control character spelled out the way JSON
1785
- * does (`\n`, `\u001b`), C1 controls and the Unicode line separators
1786
- * included. The memory block and `stratus memory list` both frame an entry
1787
- * by its line — `- ` under a region heading, an indent under an id — and a
1788
- * fact holding a newline could otherwise start a line of its own that reads
1789
- * as a heading, the trusted region's heading among them. Escaped rather
1790
- * than dropped, so what is shown is still what is stored.
2696
+ * does (`\n`, `\u001b`), C1 controls, the Unicode line separators and
2697
+ * the bidi controls included. The memory block and `stratus memory list`
2698
+ * both frame an entry by its line — `- ` under a region heading, an indent
2699
+ * under an id — and a fact holding a newline could otherwise start a line
2700
+ * of its own that reads as a heading, the trusted region's heading among
2701
+ * them; a right-to-left override would make the line read differently to
2702
+ * a person than to the model. Escaped rather than dropped, so what is
2703
+ * shown is still what is stored.
1791
2704
  */
1792
2705
  export declare const escapeControlCharacters: (text: string) => string;
1793
- 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;
1794
2723
  /**
1795
2724
  * The skills block: one line per enabled skill — id, name, and the
1796
2725
  * description routing runs on. This is the whole point of the step: the
@@ -1982,6 +2911,78 @@ export interface ObserveInput {
1982
2911
  */
1983
2912
  metadata?: JsonObject;
1984
2913
  }
2914
+ /**
2915
+ * Thrown by a provider when the request did not fit the model's context
2916
+ * window — the transcript, not the answer, is what was too long.
2917
+ *
2918
+ * A distinct type because it is the one provider failure the kernel can do
2919
+ * something about. Everything else a provider throws is final; this one
2920
+ * says "the same request will never work, a shorter one might", and the
2921
+ * runner answers by sending less history (see `raiseContextFloor`).
2922
+ *
2923
+ * Providers opt in. Recognising it means matching a vendor's own wording
2924
+ * for a 400, which only the adapter can do — and a harness provider that
2925
+ * manages its own context (`provider-claude-code`, `provider-codex`) has
2926
+ * nothing to report, because the transcript it sends is not ours to
2927
+ * shorten. A provider that never throws this behaves exactly as it did.
2928
+ */
2929
+ export declare class ContextOverflowError extends Error {
2930
+ constructor(message: string);
2931
+ }
2932
+ /**
2933
+ * How many of a session's oldest messages are no longer sent to the
2934
+ * provider.
2935
+ *
2936
+ * Durable, and **monotonic** like the trust label: a transcript only grows,
2937
+ * so a floor that has had to rise once would have to rise again on the next
2938
+ * turn, and re-discovering that costs a rejected request every time. Raised
2939
+ * only by `raiseContextFloor`, never lowered — a conversation does not get
2940
+ * shorter.
2941
+ *
2942
+ * It bounds what is *sent*, never what is stored. The transcript is the
2943
+ * record of what happened and stays whole on disk; this is a window onto
2944
+ * its tail. `stratus session rollover` is still the way to actually leave a
2945
+ * conversation behind.
2946
+ */
2947
+ export declare const CONTEXT_FLOOR_METADATA_KEY = "contextFloor";
2948
+ /** The stored floor, or 0 for a session that has never overflowed. */
2949
+ export declare const contextFloorOf: (session: Pick<Session, "metadata">) => number;
2950
+ /**
2951
+ * The note that stands in for the messages a floor cut away, prefixed to
2952
+ * the oldest message the model still sees.
2953
+ *
2954
+ * Said rather than done silently, for the reason every other cut in this
2955
+ * codebase is announced: a model that cannot tell a conversation that
2956
+ * started here from one that was trimmed will answer as though the earlier
2957
+ * turns never happened, and confidently.
2958
+ */
2959
+ export declare const droppedTurnsNote: (count: number) => string;
2960
+ /**
2961
+ * The messages above `session`'s floor, with the
2962
+ * note in place of what was cut. The session itself is untouched — the
2963
+ * copy is shallow and deliberately shares `metadata`, because providers
2964
+ * keep replay state there (the Anthropic raw-turn cache) and a turn that
2965
+ * wrote into a copy would lose it.
2966
+ *
2967
+ * Returns the session unchanged when the floor is 0, so a conversation that
2968
+ * has never overflowed is not copied on every turn.
2969
+ */
2970
+ export declare const messagesWithinContextFloor: (session: Session) => readonly Message[];
2971
+ /**
2972
+ * Raise the floor so the next attempt sends less, and report whether there
2973
+ * was anything left to give up.
2974
+ *
2975
+ * Halves the remaining window each time rather than trimming a fixed
2976
+ * amount: nothing here knows how many tokens a message is, and an overflow
2977
+ * says only "too big", not "too big by this much" — so the cheap thing is
2978
+ * to converge in a handful of attempts rather than to creep towards the
2979
+ * answer one turn at a time, paying a rejected request for each step.
2980
+ *
2981
+ * Returns false when the window is already down to the newest turn. That is
2982
+ * the floor's floor: a single turn that does not fit is a message larger
2983
+ * than the model can read, and no amount of dropping history fixes it.
2984
+ */
2985
+ export declare const raiseContextFloor: (session: Session) => boolean;
1985
2986
  /**
1986
2987
  * Thrown when a run is stopped by its abort signal. The session ends up
1987
2988
  * `failed` with this error's message as `lastError`, so an aborted turn is
@@ -2045,6 +3046,18 @@ export interface AgentRunnerOptions {
2045
3046
  */
2046
3047
  streaming?: boolean;
2047
3048
  }
3049
+ /**
3050
+ * Tool turns one message may take before it wraps up. The harness providers
3051
+ * spend the same number as their own inner budget, so they import it rather
3052
+ * than keep a copy.
3053
+ *
3054
+ * 8 until the wrap-up turn existed, and 8 was the whole budget for one Slack
3055
+ * message: an agent with a shell spent it on a handful of commands and
3056
+ * failed with nothing said. With a wrap-up turn the ceiling is where a long
3057
+ * task checks in rather than where it dies, and 40 is enough for real
3058
+ * multi-step work while still bounding what a runaway loop can spend.
3059
+ */
3060
+ export declare const DEFAULT_MAX_TURNS = 40;
2048
3061
  export declare class AgentRunner {
2049
3062
  readonly bus: EventBus;
2050
3063
  readonly store: SessionStore;