@ethosagent/core 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,175 @@
1
1
  import * as _ethosagent_types from '@ethosagent/types';
2
- import { HookRegistry, LLMProvider, ToolRegistry, PersonalityRegistry, MemoryProvider, SessionStore, ContextInjector, Storage, Session, SessionFilter, StoredMessage, SessionUsage, SearchResult, MemoryLoadContext, MemoryUpdate, PersonalityConfig, VoidHooks, ModifyingHooks, ClaimingHooks, Message, ToolDefinitionLite, CompletionOptions, CompletionChunk, Tool, ToolFilterOpts, ToolContext, ToolResult } from '@ethosagent/types';
2
+ import { ClarifyStore, PendingClarify, ClarifyResponse, ClarifyAnswerableBy, ClarifySurfaceType, PersonalityObservabilityConfig, SpanKind, EventSeverity, HookRegistry, LLMProvider, ToolRegistry, PersonalityRegistry, MemoryProvider, SessionStore, ContextInjector, Storage, ContextEngineRegistry, RequestDumpStore, SteerSink, Attachment, KeyValueStore, SecretRef, ToolCapabilities, ToolContext, Tool, PersonalityConfig, ContextEngine, ContextEngineCompactInput, ContextEngineCompactOutput, Message, Session, SessionFilter, StoredMessage, SessionUsage, SearchResult, CompressionEvent, MemoryContext, MemorySnapshot, MemoryEntry, SearchOpts, MemoryUpdate, ListOpts, MemoryEntryRef, VoidHooks, ModifyingHooks, ClaimingHooks, ToolDefinitionLite, CompletionOptions, CompletionChunk, LLMProviderRegistry, LLMProviderFactory, MemoryProviderRegistry, MemoryProviderFactory, RequestDumpRecord, ScopedFetch, ScopedFs, ScopedFsEntry, ScopedProcess, SpawnOpts, ProcessResult, ScopedSecretsResolver, ToolFilterOpts, ToolResult } from '@ethosagent/types';
3
+ export { MemoryConflictError } from '@ethosagent/types';
4
+ import * as _ethosagent_safety_watcher from '@ethosagent/safety-watcher';
5
+ import { InjectionClassifier } from '@ethosagent/safety-injection';
6
+ import { NetworkPolicy } from '@ethosagent/safety-network';
3
7
 
8
+ /** Raised by `request()` when a clarify is already pending for the session (plan Q5). */
9
+ declare class ClarifyBusyError extends Error {
10
+ readonly code: "CLARIFY_BUSY";
11
+ constructor();
12
+ }
13
+ /** Raised by `request()` when the timeout fires and no `default` was provided (plan Q4/C). */
14
+ declare class ClarifyTimedOutNoDefaultError extends Error {
15
+ readonly code: "CLARIFY_TIMED_OUT_NO_DEFAULT";
16
+ constructor();
17
+ }
18
+ /** Raised by `request()` when no interactive surface has registered a presenter. */
19
+ declare class ClarifyNoSurfaceError extends Error {
20
+ readonly code: "CLARIFY_NO_SURFACE";
21
+ constructor();
22
+ }
23
+ interface ClarifyRequestInput {
24
+ question: string;
25
+ options?: string[];
26
+ default?: string;
27
+ timeoutMs: number;
28
+ answerableBy: ClarifyAnswerableBy;
29
+ sessionId: string;
30
+ surfaceType: ClarifySurfaceType;
31
+ surfaceContext?: Record<string, unknown>;
32
+ /** When the turn aborts, the pending clarify resolves as cancelled. */
33
+ abortSignal?: AbortSignal;
34
+ }
35
+ /** A surface registers this to present a pending clarify to the user. */
36
+ type ClarifyPresenter = (req: PendingClarify) => void | Promise<void>;
37
+ /**
38
+ * Fired when a pending clarify resolves (user answer, timeout, or cancel) —
39
+ * surfaces use it to tear down the prompt/modal/card they presented. The
40
+ * `row` carries the session id and request id; `response` is `null` for the
41
+ * timeout-no-default case (no answer was produced).
42
+ */
43
+ type ClarifyResolvedListener = (row: PendingClarify, response: ClarifyResponse | null) => void;
44
+ declare class ClarifyBridge {
45
+ readonly store: ClarifyStore;
46
+ private readonly pending;
47
+ private presenter;
48
+ private readonly resolvedListeners;
49
+ /**
50
+ * `store` is exposed read-only so a surface (e.g. TelegramClarifySurface)
51
+ * can patch `surfaceContext` after presenting the prompt and look up rows
52
+ * by id without proxying every call through the bridge.
53
+ */
54
+ constructor(store: ClarifyStore);
55
+ /** A surface registers how it presents a pending clarify to the user. */
56
+ setPresenter(presenter: ClarifyPresenter): void;
57
+ /**
58
+ * Subscribe to clarify resolutions so a surface can tear down its prompt
59
+ * when the request is answered, times out, or is cancelled. Returns an
60
+ * unsubscribe function.
61
+ */
62
+ onResolved(listener: ClarifyResolvedListener): () => void;
63
+ /** True iff a clarify is currently pending for the given session. */
64
+ hasPending(sessionId: string): boolean;
65
+ /** Pending rows still awaiting an answer — for SSE reconnect re-presentation. */
66
+ listPending(sessionId?: string): PendingClarify[];
67
+ /**
68
+ * Persisted pending rows from the store — for boot-time hydration (a surface
69
+ * that outlives a single process needs to find rows that survived a
70
+ * restart). `listPending()` only sees in-memory rows; this is the source of
71
+ * truth across restarts.
72
+ */
73
+ listPersisted(filter?: {
74
+ surfaceType?: string;
75
+ sessionId?: string;
76
+ }): Promise<PendingClarify[]>;
77
+ /**
78
+ * Issue a clarify request. Resolves when the user answers, the timeout fires
79
+ * (with `default`), or the turn aborts (as cancelled). Rejects with
80
+ * `ClarifyBusyError` if one is already pending for the session, or with
81
+ * `ClarifyTimedOutNoDefaultError` on timeout when no `default` was given.
82
+ */
83
+ request(input: ClarifyRequestInput): Promise<ClarifyResponse>;
84
+ /**
85
+ * Resolve a pending clarify. Called by a surface when the user answers or
86
+ * cancels, and internally on timeout. Unknown / already-resolved ids are
87
+ * swallowed (another surface or the timeout beat this one).
88
+ *
89
+ * Degraded-mode fallback: when no in-process entry exists but the row is
90
+ * still persisted (gateway crashed mid-clarify, then the user tapped the
91
+ * button after restart), still clear the row and notify listeners so the
92
+ * surface can edit its UI to the resolved state. The original `request()`
93
+ * promise is gone — the agent waiting on it died with the process — so
94
+ * the answer can't reach the LLM, but at least the visible prompt updates.
95
+ */
96
+ respond(response: ClarifyResponse): Promise<void>;
97
+ private notifyResolved;
98
+ /**
99
+ * Restart recovery: fire timeout responses for any persisted rows that have
100
+ * already passed their deadline. Called on boot and on an interval by
101
+ * surfaces that outlive a single turn (web-api, gateway).
102
+ *
103
+ * Listeners are notified for swept rows so surfaces can edit their UI in
104
+ * place — a card whose prompt timed out while the process was down should
105
+ * still update to the "timed out" state instead of hanging on buttons.
106
+ */
107
+ sweep(now?: Date): Promise<void>;
108
+ private fireTimeout;
109
+ }
110
+
111
+ interface RecordEventOpts {
112
+ traceId?: string;
113
+ spanId?: string;
114
+ code?: string;
115
+ cause?: string;
116
+ details?: Record<string, unknown>;
117
+ severity?: EventSeverity;
118
+ }
119
+ interface AgentLoopObservability {
120
+ startTurnTrace(opts: {
121
+ sessionId?: string;
122
+ personalityId?: string;
123
+ snapshotId?: string;
124
+ obsConfig?: PersonalityObservabilityConfig;
125
+ attrs?: Record<string, unknown>;
126
+ }): string;
127
+ endTrace(traceId: string, status: 'ok' | 'error' | 'aborted'): void;
128
+ startSpan(opts: {
129
+ traceId: string;
130
+ parentSpanId?: string;
131
+ kind: SpanKind;
132
+ name: string;
133
+ attrs?: Record<string, unknown>;
134
+ obsConfig?: PersonalityObservabilityConfig;
135
+ }): string;
136
+ endSpan(spanId: string, status: 'ok' | 'error' | 'blocked', attrs?: Record<string, unknown>): void;
137
+ recordSafetyBlock(opts: RecordEventOpts): void;
138
+ recordCompaction(opts: RecordEventOpts): void;
139
+ recordTierEscalation(opts: RecordEventOpts & {
140
+ from: string;
141
+ to: string;
142
+ reason: string;
143
+ personalityId: string;
144
+ }): void;
145
+ recordTierOverride(opts: RecordEventOpts & {
146
+ actor: 'user' | 'framework';
147
+ tier: string;
148
+ personalityId: string;
149
+ }): void;
150
+ flush(): void;
151
+ }
152
+
153
+ declare const KNOWN_AGENT_EVENT_TYPES: readonly ["text_delta", "thinking_delta", "tool_start", "tool_progress", "tool_end", "usage", "error", "done", "context_meta", "run_start"];
154
+ type KnownAgentEventType = (typeof KNOWN_AGENT_EVENT_TYPES)[number];
155
+ /**
156
+ * Returns true when the event's `type` is one a current consumer knows
157
+ * about. Useful for development-mode warnings:
158
+ *
159
+ * for await (const event of loop.run(...)) {
160
+ * if (!isKnownAgentEvent(event)) {
161
+ * console.warn('Unknown AgentEvent type:', event.type);
162
+ * continue;
163
+ * }
164
+ * switch (event.type) { ... }
165
+ * }
166
+ *
167
+ * Production code should silently skip unknown events; this helper is for
168
+ * test runs and dev surfaces that want to alert on newly-added variants.
169
+ */
170
+ declare function isKnownAgentEvent(event: {
171
+ type: string;
172
+ }): event is AgentEvent;
4
173
  type AgentEvent = {
5
174
  type: 'text_delta';
6
175
  text: string;
@@ -17,14 +186,14 @@ type AgentEvent = {
17
186
  toolName: string;
18
187
  message: string;
19
188
  percent?: number;
20
- audience: 'internal' | 'user';
189
+ audience: 'internal' | 'user' | 'dashboard';
21
190
  } | {
22
191
  type: 'tool_end';
23
192
  toolCallId: string;
24
193
  toolName: string;
25
194
  ok: boolean;
26
195
  durationMs: number;
27
- audience?: 'internal' | 'user';
196
+ audience?: 'internal' | 'user' | 'dashboard';
28
197
  /**
29
198
  * Tool output body — the success value when `ok`, or the error
30
199
  * message when `ok: false`. Optional so consumers that only care
@@ -67,6 +236,12 @@ interface AgentLoopConfig {
67
236
  tools?: ToolRegistry;
68
237
  personalities?: PersonalityRegistry;
69
238
  memory?: MemoryProvider;
239
+ /**
240
+ * Phase 3 — team id. When set, AgentLoop stamps `teamId` on every
241
+ * `ToolContext` so team memory tools can route to the correct team scope.
242
+ * Absent when running solo.
243
+ */
244
+ teamId?: string;
70
245
  session?: SessionStore;
71
246
  hooks?: HookRegistry;
72
247
  injectors?: ContextInjector[];
@@ -90,7 +265,63 @@ interface AgentLoopConfig {
90
265
  * `storage` is set.
91
266
  */
92
267
  dataDir?: string;
268
+ /**
269
+ * Optional observability adapter. When provided, AgentLoop records traces,
270
+ * spans, and events for LLM calls, tool calls, and errors via typed
271
+ * domain helpers. When absent, behaviour is identical to before — no
272
+ * observability writes occur.
273
+ */
274
+ observability?: AgentLoopObservability;
275
+ /**
276
+ * Ch.3c — Tier-2 LLM injection classifier. When provided, AgentLoop calls
277
+ * it after wrapping any `outputIsUntrusted` tool result whose Tier-1
278
+ * pattern check fired, whose content is > 500 chars, or when the active
279
+ * personality's `safety.injectionDefense.classifier.alwaysCallLLM` is set.
280
+ * When unset, only Tier-1 (regex) classification runs.
281
+ */
282
+ injectionClassifier?: InjectionClassifier;
283
+ /**
284
+ * Ch.6a — In-process watcher. When provided, AgentLoop forwards every
285
+ * tool_start / tool_end / usage event into watcher.observe() and acts
286
+ * on non-`allow` decisions:
287
+ * - `terminate` → yield an `error` event and end the turn
288
+ * - `pause` → yield a user-visible `tool_progress` chip and end
289
+ * the turn (the user's next message resumes; the
290
+ * watcher's state is fresh per run via resetTurn())
291
+ * - `force_approval` → set a per-iteration flag that promotes the
292
+ * next tool to requiresApproval (TODO — needs the
293
+ * approval-hook plumbing; for v1 we treat it as
294
+ * `pause` to fail safe)
295
+ * `allow` is the no-op path. Watcher decisions are recorded as
296
+ * `audit.watcher` events on the optional ObservabilityWriter.
297
+ */
298
+ watcher?: _ethosagent_safety_watcher.Watcher;
93
299
  modelRouting?: Record<string, string>;
300
+ /**
301
+ * Per-personality memory provider registry. Maps provider names ('markdown',
302
+ * 'vector', plugin-registered names) to factory functions. When a personality
303
+ * declares `memory.provider`, AgentLoop resolves from this map.
304
+ */
305
+ memoryProviders?: Map<string, (options?: Record<string, unknown>) => MemoryProvider | Promise<MemoryProvider>>;
306
+ /**
307
+ * E4 — Pluggable context-engine registry. When unset, AgentLoop builds
308
+ * a `DefaultContextEngineRegistry` (drop_oldest + semantic_summary
309
+ * placeholder + reference_preserving). Each personality picks an engine
310
+ * via `personality.context_engine`; unknown names fall back to
311
+ * `drop_oldest` with a one-line warning.
312
+ */
313
+ contextEngines?: ContextEngineRegistry;
314
+ /**
315
+ * Bridge for the `clarify` tool — the agent asks the user a structured
316
+ * question mid-turn and waits. Optional: when unset, the `clarify` tool
317
+ * reports `CLARIFY_NO_SURFACE` and the agent falls back to plain prose.
318
+ */
319
+ clarifyBridge?: ClarifyBridge;
320
+ /**
321
+ * Optional request dump store. When provided, AgentLoop appends a full
322
+ * record of each LLM request/response for offline analysis and debugging.
323
+ */
324
+ requestDumpStore?: RequestDumpStore;
94
325
  options?: {
95
326
  maxIterations?: number;
96
327
  historyLimit?: number;
@@ -128,6 +359,26 @@ interface RunOptions {
128
359
  * `MAX_SPAWN_DEPTH` can be enforced across recursive sub-agent calls.
129
360
  */
130
361
  agentId?: string;
362
+ /**
363
+ * FW-9 — `steer` busy-input mode. Surfaces (CLI REPL) push user-typed text
364
+ * here while the agent is mid-turn. AgentLoop drains the sink at the
365
+ * iteration seam (after tool_results land, before the next LLM call) and
366
+ * folds each entry in as a `[USER STEER]: <text>` text block on the user
367
+ * message carrying the tool_results.
368
+ *
369
+ * Pre-first-iteration (no tool_results yet) and idle (no run in flight)
370
+ * steering falls back to `queue` at the surface, never reaching AgentLoop.
371
+ */
372
+ steerSink?: SteerSink;
373
+ /** Per-turn inbound attachments from the user message. Persisted as an
374
+ * `<attachments>` annotation prepended to the user text. Threaded to the
375
+ * capability resolver via `ToolRegistry.setTurnAttachments()`. */
376
+ attachments?: _ethosagent_types.Attachment[];
377
+ /**
378
+ * Override model tier for this run only (from /tier command).
379
+ * Consumed once; does not persist across runs.
380
+ */
381
+ tierOverride?: _ethosagent_types.ModelTierName;
131
382
  }
132
383
  declare class AgentLoop {
133
384
  private readonly llm;
@@ -151,11 +402,30 @@ declare class AgentLoop {
151
402
  private readonly maxIdenticalToolCalls;
152
403
  private readonly streamingTimeoutMs;
153
404
  private readonly modelRouting;
405
+ private readonly memoryProviders;
154
406
  private readonly storage?;
155
407
  private readonly dataDir?;
408
+ private readonly observability?;
409
+ private readonly injectionClassifier?;
410
+ private readonly watcher?;
411
+ private readonly contextEngines;
412
+ /** Bridge for the `clarify` tool; undefined when no interactive surface is wired. */
413
+ readonly clarifyBridge?: ClarifyBridge;
414
+ /** Optional request dump store for full LLM request/response recording. */
415
+ private readonly requestDumpStore?;
416
+ /** Phase 3 — team id stamped onto ToolContext when loop runs inside a team. */
417
+ private readonly teamId?;
156
418
  /** Per-session accumulated spend in USD. Keyed by sessionKey. Reset via resetSessionCost(). */
157
419
  private readonly sessionCosts;
420
+ /** FW-28 — per-session mtime registry. Keyed by sessionKey → (absPath → record). */
421
+ private readonly sessionReadMtimes;
158
422
  constructor(config: AgentLoopConfig);
423
+ /**
424
+ * Resolve a pending clarify request — called by an interactive surface when
425
+ * the user answers or cancels. No-op when no clarify bridge is wired or the
426
+ * request id is unknown (already resolved / timed out).
427
+ */
428
+ respondToClarify(response: _ethosagent_types.ClarifyResponse): Promise<void>;
159
429
  /** Returns all available tools for inventory display (e.g. TUI splash screen). */
160
430
  getAvailableTools(): _ethosagent_types.Tool[];
161
431
  /** Returns all registered personalities for inventory display. */
@@ -166,15 +436,118 @@ declare class AgentLoop {
166
436
  getSessionCost(sessionKey: string): number;
167
437
  /** Resets the session spend counter — call after /new or /personality switch. */
168
438
  resetSessionCost(sessionKey: string): void;
439
+ /**
440
+ * Resolve the effective model for an LLM call, respecting tier config.
441
+ * Returns the model string to pass as modelOverride, and the tier name used.
442
+ */
443
+ private resolveModelWithTier;
169
444
  run(text: string, opts?: RunOptions): AsyncGenerator<AgentEvent>;
170
445
  private handleChunk;
446
+ private dedupHistory;
171
447
  private toLLMMessages;
448
+ private handleUntrustedResult;
449
+ private maybeCompact;
172
450
  private buildScopedStorage;
173
451
  }
174
452
 
453
+ declare function buildAttachmentAnnotation(attachments: Attachment[]): string;
454
+
455
+ interface CapabilityBackends {
456
+ kvStoreFactory?: (tool: string, scopeId: string) => KeyValueStore;
457
+ secretsBackend?: (ref: SecretRef) => Promise<string>;
458
+ storage?: Storage;
459
+ personalityFsReach?: {
460
+ read: string[];
461
+ write: string[];
462
+ };
463
+ /**
464
+ * Full personality network policy. The `allow` list is intersected
465
+ * with each tool's declared `allowedHosts`; `deny` and
466
+ * `allow_private_urls` plus the always-on safety floor (cloud-metadata,
467
+ * private-network, scheme, DNS-rebinding) flow through `safeFetch`.
468
+ */
469
+ personalityNetworkPolicy?: NetworkPolicy;
470
+ attachmentCache?: _ethosagent_types.AttachmentCache;
471
+ inboundAttachments?: _ethosagent_types.Attachment[];
472
+ }
473
+ type ResolvedFields = Partial<Pick<ToolContext, 'kvStore' | 'secretsResolver' | 'scopedFetch' | 'scopedFs' | 'scopedProcess' | 'attachments'>>;
474
+ interface CapabilityScopeIds {
475
+ sessionId: string;
476
+ personalityId?: string;
477
+ }
478
+ declare function resolveCapabilities(toolName: string, capabilities: ToolCapabilities | undefined, scopeIds: CapabilityScopeIds, backends: CapabilityBackends): ResolvedFields;
479
+
480
+ interface CapabilityValidationError {
481
+ tool: string;
482
+ capability: string;
483
+ message: string;
484
+ }
485
+ declare function validateRegistration(tool: Tool, personality: PersonalityConfig): CapabilityValidationError[];
486
+
487
+ declare class FileClarifyStore implements ClarifyStore {
488
+ private readonly storage;
489
+ private readonly root;
490
+ private readonly pendingPath;
491
+ /** Serializes the read-modify-write cycle within this process. */
492
+ private mutex;
493
+ /** `root` is the absolute `~/.ethos/clarify` directory (caller-resolved). */
494
+ constructor(storage: Storage, root: string);
495
+ add(req: PendingClarify): Promise<void>;
496
+ get(requestId: string): Promise<PendingClarify | null>;
497
+ list(filter?: {
498
+ surfaceType?: string;
499
+ sessionId?: string;
500
+ }): Promise<PendingClarify[]>;
501
+ remove(requestId: string): Promise<void>;
502
+ update(requestId: string, patch: Partial<PendingClarify>): Promise<void>;
503
+ expired(now: Date): Promise<PendingClarify[]>;
504
+ private readAll;
505
+ /** Run a read-modify-write under the per-process mutex with an atomic write. */
506
+ private mutate;
507
+ }
508
+
509
+ declare class DropOldestEngine implements ContextEngine {
510
+ readonly name = "drop_oldest";
511
+ compact(opts: ContextEngineCompactInput): Promise<ContextEngineCompactOutput>;
512
+ }
513
+
514
+ declare class ReferencePreservingEngine implements ContextEngine {
515
+ readonly name = "reference_preserving";
516
+ compact(opts: ContextEngineCompactInput): Promise<ContextEngineCompactOutput>;
517
+ }
518
+
519
+ type SummarizerFn = (middle: Message[], targetTokens: number) => Promise<string>;
520
+ declare class SemanticSummaryEngine implements ContextEngine {
521
+ readonly name = "semantic_summary";
522
+ private readonly summarize;
523
+ constructor(opts?: {
524
+ summarize?: SummarizerFn;
525
+ });
526
+ compact(opts: ContextEngineCompactInput): Promise<ContextEngineCompactOutput>;
527
+ }
528
+
529
+ interface DefaultContextEngineRegistryOptions {
530
+ /** Optional summarizer wired into the SemanticSummaryEngine. Without it
531
+ * that engine falls back to a placeholder summary (no LLM call). */
532
+ summarize?: SummarizerFn;
533
+ }
534
+ declare class DefaultContextEngineRegistry implements ContextEngineRegistry {
535
+ private readonly engines;
536
+ constructor(opts?: DefaultContextEngineRegistryOptions);
537
+ register(engine: ContextEngine): void;
538
+ get(name: string): ContextEngine | undefined;
539
+ names(): string[];
540
+ }
541
+
542
+ declare function estimateTokens(text: string): number;
543
+ declare function estimateMessageTokens(message: Message): number;
544
+ declare function estimateMessagesTokens(input: Message | Message[] | string): number;
545
+
175
546
  declare class InMemorySessionStore implements SessionStore {
176
547
  private sessions;
177
548
  private messages;
549
+ private compressions;
550
+ private turnState;
178
551
  private idCounter;
179
552
  createSession(data: Omit<Session, 'id' | 'createdAt' | 'updatedAt'>): Promise<Session>;
180
553
  getSession(id: string): Promise<Session | null>;
@@ -192,13 +565,36 @@ declare class InMemorySessionStore implements SessionStore {
192
565
  limit?: number;
193
566
  sessionId?: string;
194
567
  }): Promise<SearchResult[]>;
568
+ recordCompression(event: Omit<CompressionEvent, 'id' | 'createdAt'>): Promise<CompressionEvent>;
569
+ listCompressions(sessionId: string): Promise<CompressionEvent[]>;
570
+ recordTurnStart(sessionId: string): Promise<{
571
+ turnNumber: number;
572
+ lastCompactionTurn: number;
573
+ }>;
574
+ recordCompactionTurn(sessionId: string, turnNumber: number): Promise<void>;
195
575
  pruneOldSessions(olderThan: Date): Promise<number>;
196
576
  vacuum(): Promise<void>;
197
577
  }
198
578
 
579
+ interface InMemoryToolContextOptions {
580
+ sessionId?: string;
581
+ sessionKey?: string;
582
+ platform?: string;
583
+ workingDir?: string;
584
+ personalityId?: string;
585
+ currentTurn?: number;
586
+ messageCount?: number;
587
+ resultBudgetChars?: number;
588
+ withStorage?: boolean;
589
+ }
590
+ declare function makeTestToolContext(opts?: InMemoryToolContextOptions): ToolContext;
591
+
199
592
  declare class NoopMemoryProvider implements MemoryProvider {
200
- prefetch(_ctx: MemoryLoadContext): Promise<null>;
201
- sync(_ctx: MemoryLoadContext, _updates: MemoryUpdate[]): Promise<void>;
593
+ prefetch(_ctx: MemoryContext): Promise<MemorySnapshot | null>;
594
+ read(_key: string, _ctx: MemoryContext): Promise<MemoryEntry | null>;
595
+ search(_query: string, _ctx: MemoryContext, _opts?: SearchOpts): Promise<MemoryEntry[]>;
596
+ sync(_updates: MemoryUpdate[], _ctx: MemoryContext): Promise<void>;
597
+ list(_ctx: MemoryContext, _opts?: ListOpts): Promise<MemoryEntryRef[]>;
202
598
  }
203
599
 
204
600
  declare class DefaultPersonalityRegistry implements PersonalityRegistry {
@@ -234,6 +630,115 @@ declare class DefaultHookRegistry implements HookRegistry {
234
630
  private remove;
235
631
  }
236
632
 
633
+ /**
634
+ * Pass-through decorator that makes the wiring intent explicit: this provider
635
+ * uses eager prefetch (all content injected at session start). The AgentLoop
636
+ * already calls `prefetch()` on every session open; this wrapper delegates all
637
+ * five methods unchanged. Used for personality memory.
638
+ */
639
+ declare class EagerPrefetchPolicy implements MemoryProvider {
640
+ private readonly inner;
641
+ constructor(inner: MemoryProvider);
642
+ prefetch(ctx: MemoryContext): Promise<MemorySnapshot | null>;
643
+ read(key: string, ctx: MemoryContext): Promise<MemoryEntry | null>;
644
+ search(query: string, ctx: MemoryContext, opts?: SearchOpts): Promise<MemoryEntry[]>;
645
+ sync(updates: MemoryUpdate[], ctx: MemoryContext): Promise<void>;
646
+ list(ctx: MemoryContext, opts?: ListOpts): Promise<MemoryEntryRef[]>;
647
+ }
648
+ /**
649
+ * Suppresses bulk prefetch. `prefetch()` returns null so the AgentLoop does
650
+ * not inject all content at session start. The existing
651
+ * `createTeamMemoryIndexInjector` in wiring handles the lightweight topic-index
652
+ * injection via `list()`. All other methods delegate unchanged.
653
+ *
654
+ * Used for team memory: agents see a topic index and load content on demand via
655
+ * team_memory_read.
656
+ */
657
+ declare class LazyOnDemandPolicy implements MemoryProvider {
658
+ private readonly inner;
659
+ constructor(inner: MemoryProvider);
660
+ prefetch(_ctx: MemoryContext): Promise<MemorySnapshot | null>;
661
+ read(key: string, ctx: MemoryContext): Promise<MemoryEntry | null>;
662
+ search(query: string, ctx: MemoryContext, opts?: SearchOpts): Promise<MemoryEntry[]>;
663
+ sync(updates: MemoryUpdate[], ctx: MemoryContext): Promise<void>;
664
+ list(ctx: MemoryContext, opts?: ListOpts): Promise<MemoryEntryRef[]>;
665
+ }
666
+ /**
667
+ * Wraps `sync()` with an optimistic-concurrency precondition check.
668
+ *
669
+ * The policy records the `mtime` (from `MemoryEntry.metadata.lastUpdatedAt`)
670
+ * each time `read()` or `search()` returns an entry, keyed by
671
+ * `${scopeId}:${key}`. When `sync()` is called for a key the policy has a
672
+ * read-timestamp for, it re-reads the current `mtime` from the inner provider
673
+ * and rejects the write with a `MemoryConflictError` if the file has been
674
+ * modified since the caller last saw it.
675
+ *
676
+ * Keys never read (no timestamp recorded) pass through unconditionally —
677
+ * this avoids blocking blind adds on new keys.
678
+ *
679
+ * **Known limitation — not fully atomic:** the mtime check and the subsequent
680
+ * `inner.sync()` are not a single atomic operation. Two concurrent writers
681
+ * that both pass the mtime check in the same millisecond can both write. This
682
+ * is an inherent property of the decorator approach over a filesystem backend;
683
+ * a fully atomic compare-and-swap would require support in the storage layer.
684
+ * The policy catches the common case of sequential-but-concurrent agents where
685
+ * writes are spaced by network and tool-call latency.
686
+ *
687
+ * **Instance scope:** one `LastWriteWinsPolicy` instance tracks timestamps for
688
+ * one caller (typically one AgentLoop / session). Do not share an instance
689
+ * across concurrent callers — the read-timestamp map is not thread-isolated and
690
+ * cross-caller reads would invalidate each other's preconditions.
691
+ *
692
+ * Used for team memory to prevent silent overwrites when two agents write the
693
+ * same file within the same session boundary.
694
+ */
695
+ declare class LastWriteWinsPolicy implements MemoryProvider {
696
+ private readonly inner;
697
+ /** scopeId:key → mtime at last read (ms). */
698
+ private readonly lastReadAt;
699
+ constructor(inner: MemoryProvider);
700
+ prefetch(ctx: MemoryContext): Promise<MemorySnapshot | null>;
701
+ /** Records the entry's mtime so sync() can detect concurrent modifications. */
702
+ read(key: string, ctx: MemoryContext): Promise<MemoryEntry | null>;
703
+ /**
704
+ * Records mtimes for entries returned by search so that a write based on
705
+ * search results also benefits from conflict detection.
706
+ *
707
+ * Only sets the mtime when no prior timestamp is recorded for the key.
708
+ * Overwriting an existing timestamp from a search result would allow a
709
+ * stale write to pass: caller reads at mtime 1, external writer bumps to
710
+ * mtime 2, caller searches and the result records mtime 2, caller syncs
711
+ * stale content — conflict check passes because currentAt === recordedAt.
712
+ * Preserving the oldest (first-read) mtime ensures that risk does not apply.
713
+ */
714
+ search(query: string, ctx: MemoryContext, opts?: SearchOpts): Promise<MemoryEntry[]>;
715
+ /**
716
+ * For each key that was previously read, check the current mtime against
717
+ * the recorded read-timestamp. Rejects the entire call if any key has been
718
+ * modified by another writer since the caller last read it.
719
+ *
720
+ * After a successful write, updates `lastReadAt` baselines so that a second
721
+ * `sync()` call on the same key does not spuriously fail. Keys that were
722
+ * deleted are removed from the tracker so they can be re-added later.
723
+ */
724
+ sync(updates: MemoryUpdate[], ctx: MemoryContext): Promise<void>;
725
+ list(ctx: MemoryContext, opts?: ListOpts): Promise<MemoryEntryRef[]>;
726
+ }
727
+
728
+ /**
729
+ * Verify that `target` resolves to a path within (or equal to) `base`.
730
+ * Both paths are resolved to absolute before comparison.
731
+ *
732
+ * @throws BoundaryEscapeError if the resolved target escapes the base
733
+ */
734
+ declare function assertWithinBase(base: string, target: string): void;
735
+ declare class BoundaryEscapeError extends Error {
736
+ readonly code: "path-boundary-escape";
737
+ readonly base: string;
738
+ readonly resolved: string;
739
+ constructor(base: string, resolved: string);
740
+ }
741
+
237
742
  type PluginFactory<T, C = unknown> = (config: C) => T | null;
238
743
  declare class PluginRegistry<T, C = unknown> {
239
744
  private readonly factories;
@@ -275,11 +780,155 @@ declare class ChainedProvider implements LLMProvider {
275
780
  private activeEntry;
276
781
  }
277
782
 
783
+ declare class DefaultLLMProviderRegistry implements LLMProviderRegistry {
784
+ private readonly factories;
785
+ register(name: string, factory: LLMProviderFactory): void;
786
+ get(name: string): LLMProviderFactory | undefined;
787
+ list(): string[];
788
+ }
789
+
790
+ declare class DefaultMemoryProviderRegistry implements MemoryProviderRegistry {
791
+ private readonly factories;
792
+ register(name: string, factory: MemoryProviderFactory): void;
793
+ get(name: string): MemoryProviderFactory | undefined;
794
+ list(): string[];
795
+ }
796
+
797
+ /**
798
+ * Bounded in-memory request dump store for tests. Not suitable for production
799
+ * — use JsonlRequestDumpStore (or a durable implementation) instead.
800
+ * Caps at `maxRecords` to prevent unbounded memory growth.
801
+ */
802
+ declare class InMemoryRequestDumpStore implements RequestDumpStore {
803
+ private records;
804
+ private readonly maxRecords;
805
+ constructor(opts?: {
806
+ maxRecords?: number;
807
+ });
808
+ append(record: RequestDumpRecord): Promise<void>;
809
+ recent(opts: {
810
+ limit: number;
811
+ sessionId?: string;
812
+ since?: Date;
813
+ includeContent?: boolean;
814
+ }): Promise<RequestDumpRecord[]>;
815
+ close(): Promise<void>;
816
+ getAll(): RequestDumpRecord[];
817
+ }
818
+
819
+ /**
820
+ * Strip ANSI escape sequences from untrusted output before rendering.
821
+ * Prevents terminal manipulation via LLM-generated or tool-fetched content.
822
+ *
823
+ * Covers:
824
+ * - CSI sequences (including private modes like ?25l, ?2004h): ESC[ ... <final>
825
+ * - OSC sequences terminated with BEL (\x07) or ST (ESC\): ESC] ... BEL|ST
826
+ * - Character set selection (G0/G1): ESC( <charset>
827
+ * - Other single-character escape sequences: ESC + one char
828
+ */
829
+ declare function stripAnsiEscapes(input: string): string;
830
+
831
+ /**
832
+ * Test seam — `safeFetch`'s injection points, mirrored on the wrapper so
833
+ * tests can stub DNS + fetch hermetically. Production wiring leaves
834
+ * these undefined; `safeFetch` defaults to `node:dns/promises#lookup`
835
+ * and `globalThis.fetch`.
836
+ */
837
+ interface ScopedFetchTestSeam {
838
+ fetchImpl?: typeof fetch;
839
+ resolveHost?: (hostname: string) => Promise<string[]>;
840
+ }
841
+ /**
842
+ * Scoped network capability. Enforces two layers in order:
843
+ *
844
+ * 1. **Declared host allowlist** — the intersection of the tool's
845
+ * `capabilities.network.allowedHosts` with the personality's
846
+ * `safety.network.allow`, resolved at registration time.
847
+ * 2. **Non-overridable safety floor** — `safeFetch` runs scheme +
848
+ * cloud-metadata + private-network + per-redirect-hop revalidation
849
+ * regardless of declared hosts. A tool declaring `'*'` does NOT
850
+ * bypass the floor; a personality permitting `169.254.169.254` is
851
+ * still denied at the cloud-metadata layer.
852
+ *
853
+ * The two layers are not redundant: the allowlist is the policy
854
+ * surface tool authors and operators reason about; the floor catches
855
+ * the categories an allowlist can't (SSRF via redirect, DNS rebinding
856
+ * partial mitigation, file/data/javascript schemes).
857
+ */
858
+ declare class ScopedFetchImpl implements ScopedFetch {
859
+ private readonly allowedHosts;
860
+ private readonly policy;
861
+ private readonly testSeam;
862
+ constructor(allowedHosts: Set<string>, policy: NetworkPolicy, testSeam?: ScopedFetchTestSeam);
863
+ fetch(url: string | URL, init?: RequestInit): Promise<Response>;
864
+ private isHostAllowed;
865
+ }
866
+
867
+ /**
868
+ * Scoped filesystem capability. Enforces two layers on every call:
869
+ *
870
+ * 1. **Non-overridable deny floor** — `defaultAlwaysDeny()` lists
871
+ * `.ssh`, `.aws/credentials`, `/etc/passwd`, `/root`, etc. A path
872
+ * that touches any of these denies even when the capability and
873
+ * personality both grant the parent (mirror of
874
+ * `safety-network`'s cloud-metadata block).
875
+ *
876
+ * 2. **Declared reach allowlist** — the intersection of the tool's
877
+ * `capabilities.fs_reach` with the personality's `fs_reach`,
878
+ * resolved at registration time. Paths outside the allow set are
879
+ * rejected with `PATH_NOT_REACHABLE`.
880
+ *
881
+ * The floor cannot be disabled by configuration. Tests that need to
882
+ * exercise a forbidden path override `$HOME` before constructing the
883
+ * wrapper.
884
+ */
885
+ declare class ScopedFsImpl implements ScopedFs {
886
+ private readonly storage;
887
+ private readonly readPaths;
888
+ private readonly writePaths;
889
+ private readonly denyPaths;
890
+ constructor(storage: Storage, readPaths: Set<string>, writePaths: Set<string>);
891
+ read(path: string): Promise<string>;
892
+ readBytes(path: string): Promise<Uint8Array>;
893
+ write(path: string, content: string | Uint8Array): Promise<void>;
894
+ exists(path: string): Promise<boolean>;
895
+ list(path: string): Promise<string[]>;
896
+ mtime(path: string): Promise<number | null>;
897
+ mkdir(dir: string): Promise<void>;
898
+ listEntries(dir: string): Promise<ScopedFsEntry[]>;
899
+ private checkReach;
900
+ }
901
+
902
+ declare class ScopedProcessImpl implements ScopedProcess {
903
+ private readonly allowedBinaries;
904
+ constructor(allowedBinaries: Set<string>);
905
+ spawn(binary: string, args: string[], opts?: SpawnOpts): Promise<ProcessResult>;
906
+ }
907
+
908
+ type SecretsBackend = (ref: SecretRef) => Promise<string>;
909
+ declare class ScopedSecretsImpl implements ScopedSecretsResolver {
910
+ private readonly declaredRefs;
911
+ private readonly backend;
912
+ constructor(declaredRefs: Set<string>, backend: SecretsBackend);
913
+ get(ref: SecretRef): Promise<string>;
914
+ }
915
+
278
916
  declare class DefaultToolRegistry implements ToolRegistry {
279
917
  private readonly tools;
918
+ private readonly backends?;
919
+ constructor(backends?: CapabilityBackends);
280
920
  register(tool: Tool, opts?: {
281
921
  pluginId?: string;
282
922
  }): void;
923
+ /**
924
+ * Validate every tool reachable for this personality (per
925
+ * `toolNamesForPersonality`) against the personality's policy. Only
926
+ * the tools the personality could actually call are checked — a
927
+ * personality that doesn't list `web_search` in its toolset does not
928
+ * fail because `web_search` declared `api.exa.ai` that's missing from
929
+ * `network.allow`.
930
+ */
931
+ validateToolsForPersonality(personality: PersonalityConfig): CapabilityValidationError[];
283
932
  registerAll(tools: Tool[]): void;
284
933
  unregister(name: string): void;
285
934
  get(name: string): Tool | undefined;
@@ -303,11 +952,35 @@ declare class DefaultToolRegistry implements ToolRegistry {
303
952
  toolCallId: string;
304
953
  name: string;
305
954
  args: unknown;
306
- }>, ctx: ToolContext, allowedTools?: string[], filterOpts?: ToolFilterOpts): Promise<Array<{
955
+ }>, ctx: ToolContext, allowedTools?: string[], filterOpts?: ToolFilterOpts, turnAttachments?: _ethosagent_types.Attachment[]): Promise<Array<{
307
956
  toolCallId: string;
308
957
  name: string;
309
958
  result: ToolResult;
310
959
  }>>;
311
960
  }
312
961
 
313
- export { type AgentEvent, AgentLoop, type AgentLoopConfig, ChainedProvider, type ChainedProviderOptions, DefaultHookRegistry, DefaultPersonalityRegistry, DefaultToolRegistry, InMemorySessionStore, NoopMemoryProvider, type PluginFactory, PluginRegistry, type RunOptions };
962
+ interface ValidateUrlOptions {
963
+ /** Allow requests to localhost/127.0.0.1 (default: false).
964
+ * Useful for local LLM providers like Ollama. */
965
+ allowLocalhost?: boolean;
966
+ /** Additional hosts to allow even if they resolve to private IPs. */
967
+ trustedHosts?: string[];
968
+ }
969
+ declare class SsrfError extends Error {
970
+ constructor(url: string, reason: string);
971
+ }
972
+ /**
973
+ * Synchronous SSRF validator. Checks:
974
+ * 1. URL is well-formed
975
+ * 2. Scheme is http or https
976
+ * 3. No embedded credentials
977
+ * 4. Hostname is not a cloud metadata endpoint
978
+ * 5. If hostname is a literal IP, it must not be in a private range
979
+ * 6. Hostname is not `localhost` or `.local` / `.internal`
980
+ *
981
+ * Does NOT perform DNS resolution — use `safeFetch` from `@ethosagent/safety-network`
982
+ * for runtime fetches where DNS rebinding is a concern.
983
+ */
984
+ declare function validateUrl(urlStr: string, opts?: ValidateUrlOptions): URL;
985
+
986
+ export { type AgentEvent, AgentLoop, type AgentLoopConfig, type AgentLoopObservability, BoundaryEscapeError, type CapabilityBackends, type CapabilityScopeIds, type CapabilityValidationError, ChainedProvider, type ChainedProviderOptions, ClarifyBridge, ClarifyBusyError, ClarifyNoSurfaceError, type ClarifyPresenter, type ClarifyRequestInput, type ClarifyResolvedListener, ClarifyTimedOutNoDefaultError, DefaultContextEngineRegistry, type DefaultContextEngineRegistryOptions, DefaultHookRegistry, DefaultLLMProviderRegistry, DefaultMemoryProviderRegistry, DefaultPersonalityRegistry, DefaultToolRegistry, DropOldestEngine, EagerPrefetchPolicy, FileClarifyStore, InMemoryRequestDumpStore, InMemorySessionStore, type InMemoryToolContextOptions, KNOWN_AGENT_EVENT_TYPES, type KnownAgentEventType, LastWriteWinsPolicy, LazyOnDemandPolicy, NoopMemoryProvider, type PluginFactory, PluginRegistry, ReferencePreservingEngine, type RunOptions, ScopedFetchImpl, ScopedFsImpl, ScopedProcessImpl, ScopedSecretsImpl, type SecretsBackend, SemanticSummaryEngine, SsrfError, type SummarizerFn, type ValidateUrlOptions, assertWithinBase, buildAttachmentAnnotation, estimateMessageTokens, estimateMessagesTokens, estimateTokens, isKnownAgentEvent, makeTestToolContext, resolveCapabilities, stripAnsiEscapes, validateRegistration, validateUrl };