@salesforce/sfdx-agent-sdk 0.49.0 → 0.50.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/CHANGELOG.md CHANGED
@@ -3,6 +3,11 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-sdk` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.50.0] - 2026-08-17
7
+
8
+ ### Features
9
+ - **agent-sdk,harness-mastra,harness-claude,harness-openai**: session-context contract + conformance gate @W-23632685@ ([#747](https://github.com/forcedotcom/agentic-dx/pull/747))
10
+
6
11
  ## [0.49.0] - 2026-08-14
7
12
 
8
13
  ### Fixes
package/README.md CHANGED
@@ -159,22 +159,25 @@ keeps unparameterized call sites working.
159
159
 
160
160
  A single conversation thread.
161
161
 
162
- | Method | Signature | Description |
163
- | ------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
164
- | `getId` | `() => string` | Session/thread identifier. |
165
- | `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>` | Send a message and stream the response. The returned `eventStream` is the single iterator for the entire chat turn. |
166
- | `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<void>` | Return a consumer-executed tool result. Control message on the existing turn — post-resume events flow on the same stream. |
167
- | `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Approve a pending tool call. `{ remember: true }` ("Allow always") appends an `allow` rule to `AgentConfig.toolPolicies` and persists it before settling. Control message on the existing turn. |
168
- | `declineToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Decline a pending tool call. `{ remember: true }` ("Deny always") appends a `deny` rule and persists it before settling. Control message on the existing turn. |
169
- | `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
170
- | `clearHistory` | `() => Promise<void>` | Delete all messages. |
171
- | `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
172
- | `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response. |
173
- | `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
174
- | `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
175
- | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
176
- | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
177
- | `dispose` | `() => void` | Release session-level event resources. Idempotent. |
162
+ | Method | Signature | Description |
163
+ | ------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
164
+ | `getId` | `() => string` | Session/thread identifier. |
165
+ | `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>` | Send a message and stream the response. The returned `eventStream` is the single iterator for the entire chat turn. |
166
+ | `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<void>` | Return a consumer-executed tool result. Control message on the existing turn — post-resume events flow on the same stream. |
167
+ | `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Approve a pending tool call. `{ remember: true }` ("Allow always") appends an `allow` rule to `AgentConfig.toolPolicies` and persists it before settling. Control message on the existing turn. |
168
+ | `declineToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Decline a pending tool call. `{ remember: true }` ("Deny always") appends a `deny` rule and persists it before settling. Control message on the existing turn. |
169
+ | `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
170
+ | `clearHistory` | `() => Promise<void>` | Delete all messages. |
171
+ | `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
172
+ | `addMessages` | `(message: string \| Message[]) => Promise<void>` | Append real transcript messages (`user` / `assistant` / `tool`) to the thread **without requesting an agent response** — the write-only half of a turn. The messages persist, appear in `getMessageHistory()`, and replay to the model as prior conversation on the next `chat()`. Use it to seed earlier turns (e.g. file contents as a user message) before the first live prompt; the SDK equivalent of the service's `POST /messages` with `noReply=true`. **Not** `setSessionContext`: this writes _transcript history_ (visible in `getMessageHistory`, additive); `setSessionContext` writes an _out-of-history overlay object_ (never in history, whole-object replace). `'system'` is not a valid role here — system-level state rides `setSessionContext` / `AgentConfig.instructions`. |
173
+ | `addContext` | `(message: string \| Message[]) => Promise<void>` | **Deprecated** — renamed to `addMessages` (identical signature/behavior); delegates to it. The old name read as a sibling of `setSessionContext`, but the two are distinct channels. Will be removed in a future release; migrate to `addMessages`. |
174
+ | `setSessionContext` | `(content: SessionContext) => Promise<void>` | Replace this session's session-context object in full (whole-object set, not a merge). Persisted per-thread and durable across restart; kept out of message history, so it never appears in `getMessageHistory()`. **Currently persistence-only** — the stored object is not yet rendered into the model's system-level context; per-harness rendering on subsequent turns lands in a follow-up. Delegates to `AgentHarness.setSessionContext` — see that method's JSDoc for the full delivery/durability/isolation contract. |
175
+ | `getSessionContext` | `() => Promise<SessionContext>` | Read this session's current session-context object. Returns `{}` (an empty object) — never `null` or `undefined` — when nothing has been set on this thread yet, so callers never need a null-check. Unrelated to `getContextUsage()`, which reports context-window token occupancy, not the seeded context object. |
176
+ | `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
177
+ | `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
178
+ | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
179
+ | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
180
+ | `dispose` | `() => void` | Release session-level event resources. Idempotent. |
178
181
 
179
182
  ### `ChatStreamResult`
180
183
 
@@ -505,9 +508,15 @@ type ToolResultInfo = {
505
508
  ### Message Types
506
509
 
507
510
  ```typescript
511
+ // The message role, exported as a named type. `'system'` is not a member:
512
+ // system-level state rides `AgentConfig.instructions` and `setSessionContext`,
513
+ // never the transcript, so `getMessages` never returns a `system` message and
514
+ // one cannot be seeded via `addMessages` / `POST /messages`.
515
+ type MessageRole = 'user' | 'assistant' | 'tool';
516
+
508
517
  type Message = {
509
518
  id: string;
510
- role: 'system' | 'user' | 'assistant' | 'tool';
519
+ role: MessageRole;
511
520
  content: string | MessagePart[];
512
521
  createdAt?: Date;
513
522
  };
@@ -520,7 +529,7 @@ type ReasoningPart = { type: 'reasoning'; text: string };
520
529
 
521
530
  // Tool invocation / result segments persisted on `Message.content`. Extend the
522
531
  // `ToolCallInfo` / `ToolResultInfo` shapes with a discriminator. They appear in
523
- // message history; they are NOT valid input on `chat()` / `addContext()`.
532
+ // message history; they are NOT valid input on `chat()` / `addMessages()`.
524
533
  type ToolCallPart = ToolCallInfo & { type: 'tool-call' };
525
534
  type ToolResultPart = ToolResultInfo & { type: 'tool-result' };
526
535
 
@@ -533,9 +542,28 @@ type FilePart = { type: 'file'; mimeType: 'application/pdf'; data: string; fileN
533
542
 
534
543
  - Messages returned from `ChatSession.getMessageHistory()` always have `createdAt` populated, and the array is sorted
535
544
  ascending by `createdAt`. Consumer code can read `msg.createdAt` directly.
536
- - Consumers constructing `Message` literals for `ChatSession.addContext()` may omit `createdAt`; the SDK backfills the
545
+ - Consumers constructing `Message` literals for `ChatSession.addMessages()` may omit `createdAt`; the SDK backfills the
537
546
  current time before forwarding to the harness. Pass an explicit value to override.
538
547
 
548
+ ### Session Context Types
549
+
550
+ The per-chat session-context channel (`ChatSession.setSessionContext` / `getSessionContext`, documented in the
551
+ `ChatSession` method table above) carries a JSON-serializable object, exported as two named types:
552
+
553
+ ```typescript
554
+ // A JSON value: the transitive closure of what survives a JSON round-trip.
555
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
556
+
557
+ // The session-context object: a bare JSON object at the root (no envelope).
558
+ type SessionContext = { [key: string]: JsonValue };
559
+ ```
560
+
561
+ `setSessionContext` persists the object verbatim (so a nested / array / null-bearing object round-trips unchanged) and
562
+ `getSessionContext` returns `{}` — never `null` / `undefined` — when nothing was set. The channel is disjoint from
563
+ message history: a set object never appears in `getMessageHistory()`. Session context is the harness-agnostic channel
564
+ for system-level background state — which is why `MessageRole` (above) has no `'system'` member: there is no separate
565
+ role-capability surface to consult, because the type itself makes a `system` transcript message unconstructible.
566
+
539
567
  #### Multimodal input
540
568
 
541
569
  `ChatSession.chat()` (and the harness `stream()` it delegates to) accept either a plain string or a `MessagePart[]`. Use
@@ -570,9 +598,9 @@ await session.chat([
570
598
  },
571
599
  ]);
572
600
 
573
- // Inject multimodal context before a chat turn. `createdAt` is omitted —
574
- // the SDK backfills it before forwarding to the harness.
575
- await session.addContext([
601
+ // Seed multimodal transcript messages before a chat turn. `createdAt` is
602
+ // omitted — the SDK backfills it before forwarding to the harness.
603
+ await session.addMessages([
576
604
  {
577
605
  id: 'ctx-screenshot',
578
606
  role: 'user',
@@ -1455,7 +1483,7 @@ This package publishes two ESM entry points:
1455
1483
  | `matchesAlwaysActive` | `/harness` only | Predicate `(entries, serverName, toolName) → boolean` consulted per-tool when stamping always-load metadata or partitioning a tool-search pool. Use this instead of pattern-matching entries by hand so harness behavior stays uniform. |
1456
1484
  | `validateAlwaysActiveEntry` | `/harness` only | Throws on a malformed entry (`{}`, both fields empty). Call once per entry at the harness boundary so a typo fails loud at config time rather than silently dropping the entry on every `stream()`. |
1457
1485
  | `splitToolResultsIntoToolMessages` | `/harness` only | Read-side normalizer `(Message[]) → Message[]`. Hoists every completed tool call's `tool-result` part onto its own `role: 'tool'` message (leaving the `tool-call` on the assistant message), so `getMessages()` returns the canonical cross-harness layout. Idempotent; backfills a blank result `toolName` from the matching call; preserves `isError` and (in-memory) `error`. Call it at the end of `getMessages()`. (Whether `error` survives to the returned history is a harness-persistence concern — see `AgentHarness.getMessages`; `isError` always survives.) |
1458
- | `mergeToolResultsIntoAssistant` | `/harness` only | Write-side inverse `(Message[]) → Message[]`. Folds each `role: 'tool'` message's result back adjacent to its `tool-call` in the preceding assistant message, so a runtime that stores a completed call as one merged object round-trips losslessly. Call it at the start of `addContext()` before persisting. |
1486
+ | `mergeToolResultsIntoAssistant` | `/harness` only | Write-side inverse `(Message[]) → Message[]`. Folds each `role: 'tool'` message's result back adjacent to its `tool-call` in the preceding assistant message, so a runtime that stores a completed call as one merged object round-trips losslessly. Call it at the start of `addMessages()` before persisting. |
1459
1487
 
1460
1488
  Minimal skeleton:
1461
1489
 
@@ -4,6 +4,7 @@ import type { StreamOptions } from './harness/harness-config.js';
4
4
  import type { TelemetrySlice } from './internal/telemetry-router.js';
5
5
  import type { ChatEvent, ChatStreamResult } from './types/events.js';
6
6
  import type { Message, MessagePart } from './types/messages.js';
7
+ import type { SessionContext } from './types/session-context.js';
7
8
  import { type TelemetryBus, type TelemetryEventCallback } from './types/telemetry-events.js';
8
9
  import type { ToolPolicyRule, ToolResultInfo } from './types/tools.js';
9
10
  import type { ContextUsage } from './types/usage.js';
@@ -191,15 +192,55 @@ export interface ChatSession {
191
192
  */
192
193
  getContextUsage(): ContextUsage;
193
194
  /**
194
- * Inject context messages into the thread without triggering an LLM response.
195
- * Useful for seeding file contents, system instructions, or prior conversation
196
- * state before the user's first prompt.
195
+ * Append real conversation messages to the thread **without requesting an
196
+ * agent response** the write-only half of a normal turn. The messages
197
+ * become part of the transcript: they are persisted as `Message`s, appear in
198
+ * {@link getMessageHistory}, and are replayed to the model as prior
199
+ * conversation on the next {@link chat}. Use it to seed a thread with earlier
200
+ * `user` / `assistant` / `tool` turns (e.g. file contents surfaced as a user
201
+ * message, or a reconstructed prior conversation) before the user's first
202
+ * live prompt. This is the SDK's equivalent of the service's
203
+ * `POST /messages` with `noReply=true`, and mirrors OpenCode's
204
+ * `session.prompt({ noReply: true })` pattern.
197
205
  *
198
- * Borrowed from OpenCode's `session.prompt({ noReply: true })` pattern.
206
+ * NOT the same as {@link setSessionContext}. `addMessages` writes *transcript
207
+ * history* (visible in `getMessageHistory`, one message per call, additive).
208
+ * `setSessionContext` writes an *out-of-history overlay object* (never in
209
+ * `getMessageHistory`, whole-object replace, delivered as system-level
210
+ * background context each turn). Reach for `addMessages` when you want the
211
+ * model to see prior turns; reach for `setSessionContext` when you want to
212
+ * attach standing background state that should not read as a conversation
213
+ * message. `'system'` is not a valid role here (see {@link MessageRole}) —
214
+ * system-level state belongs on `setSessionContext` or `AgentConfig.instructions`.
199
215
  *
200
- * @param message - Context to inject (string shorthand or structured messages).
216
+ * @param message - Messages to append (string shorthand for a single `user`
217
+ * message, or structured `Message[]`).
218
+ */
219
+ addMessages(message: string | Message[]): Promise<void>;
220
+ /**
221
+ * @deprecated Renamed to {@link addMessages} (identical signature and
222
+ * behavior). The old name read as a sibling of {@link setSessionContext}, but
223
+ * the two are different channels — `addMessages` appends transcript history,
224
+ * `setSessionContext` sets an out-of-history overlay. `addContext` will be
225
+ * removed in a future release; migrate to `addMessages`.
226
+ *
227
+ * @param message - Messages to append (string shorthand or structured `Message[]`).
201
228
  */
202
229
  addContext(message: string | Message[]): Promise<void>;
230
+ /**
231
+ * Replace this session's context object in full. Delegates to
232
+ * {@link AgentHarness.setSessionContext}; see that method's JSDoc for the
233
+ * full delivery/durability/isolation contract.
234
+ *
235
+ * @param content - The full replacement session-context object.
236
+ */
237
+ setSessionContext(content: SessionContext): Promise<void>;
238
+ /**
239
+ * Read this session's current context object. `{}` if never set.
240
+ *
241
+ * @returns The last object passed to {@link setSessionContext}.
242
+ */
243
+ getSessionContext(): Promise<SessionContext>;
203
244
  /**
204
245
  * Register a callback to receive chat events in real-time. Returns an `Unsubscribe` function
205
246
  * that removes the listener. The returned function is safe to call after `dispose()` (no-op).
@@ -419,9 +460,23 @@ export declare class DefaultChatSession implements ChatSession {
419
460
  * - The formatted message MUST have a newly generated `id` from the injected `idGenerator`.
420
461
  * - The formatted message MUST have a `createdAt` timestamp from the injected `clock`.
421
462
  * - IF `message` is already an array of `Message` objects, it MUST be used directly.
422
- * - MUST delegate the final array of messages to `this.harness.addContext()`, passing `this.agentId` and `this.threadId`.
463
+ * - MUST delegate the final array of messages to `this.harness.addMessages()`, passing `this.agentId` and `this.threadId`.
423
464
  */
465
+ addMessages(message: string | Message[]): Promise<void>;
466
+ /** @deprecated Renamed to {@link addMessages}; this delegates verbatim. */
424
467
  addContext(message: string | Message[]): Promise<void>;
468
+ /**
469
+ * @requirements
470
+ * - MUST delegate to `this.harness.setSessionContext()`, passing `this.agentId`, `this.threadId`,
471
+ * and the `content` object unchanged (whole-object set, no merge).
472
+ */
473
+ setSessionContext(content: SessionContext): Promise<void>;
474
+ /**
475
+ * @requirements
476
+ * - MUST delegate to `this.harness.getSessionContext()`, passing `this.agentId` and `this.threadId`.
477
+ * - MUST return the result directly (`{}` when nothing has been set on this thread).
478
+ */
479
+ getSessionContext(): Promise<SessionContext>;
425
480
  /**
426
481
  * @requirements
427
482
  * - MUST register the provided `callback` on the internal `chatEventBus`.
@@ -401,9 +401,9 @@ export class DefaultChatSession {
401
401
  * - The formatted message MUST have a newly generated `id` from the injected `idGenerator`.
402
402
  * - The formatted message MUST have a `createdAt` timestamp from the injected `clock`.
403
403
  * - IF `message` is already an array of `Message` objects, it MUST be used directly.
404
- * - MUST delegate the final array of messages to `this.harness.addContext()`, passing `this.agentId` and `this.threadId`.
404
+ * - MUST delegate the final array of messages to `this.harness.addMessages()`, passing `this.agentId` and `this.threadId`.
405
405
  */
406
- async addContext(message) {
406
+ async addMessages(message) {
407
407
  this.assertNotDisposed();
408
408
  const messages = typeof message === 'string'
409
409
  ? [
@@ -423,7 +423,29 @@ export class DefaultChatSession {
423
423
  // same helper at their own `addContext` boundary so a
424
424
  // direct `harness.addContext` call gets the same shape.
425
425
  backfillCreatedAt(message, this.clock);
426
- await this.harness.addContext(this.agentId, this.threadId, messages);
426
+ await this.harness.addMessages(this.agentId, this.threadId, messages);
427
+ }
428
+ /** @deprecated Renamed to {@link addMessages}; this delegates verbatim. */
429
+ async addContext(message) {
430
+ await this.addMessages(message);
431
+ }
432
+ /**
433
+ * @requirements
434
+ * - MUST delegate to `this.harness.setSessionContext()`, passing `this.agentId`, `this.threadId`,
435
+ * and the `content` object unchanged (whole-object set, no merge).
436
+ */
437
+ async setSessionContext(content) {
438
+ this.assertNotDisposed();
439
+ await this.harness.setSessionContext(this.agentId, this.threadId, content);
440
+ }
441
+ /**
442
+ * @requirements
443
+ * - MUST delegate to `this.harness.getSessionContext()`, passing `this.agentId` and `this.threadId`.
444
+ * - MUST return the result directly (`{}` when nothing has been set on this thread).
445
+ */
446
+ async getSessionContext() {
447
+ this.assertNotDisposed();
448
+ return this.harness.getSessionContext(this.agentId, this.threadId);
427
449
  }
428
450
  /**
429
451
  * @requirements
@@ -8,6 +8,7 @@ import type { AgentHooks } from '../types/redaction.js';
8
8
  import type { WireCommunicationEventCallback } from '../types/wire-communication-event.js';
9
9
  import type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness-config.js';
10
10
  import type { ModelConnectivityInfo } from '../types/model-connectivity-info.js';
11
+ import type { SessionContext } from '../types/session-context.js';
11
12
  export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly [1];
12
13
  /**
13
14
  * Opt-in helper that brands a harness type with the {@link AgentConfig}
@@ -453,12 +454,99 @@ export interface AgentHarness {
453
454
  */
454
455
  clearMessages(agentId: string, threadId: string): Promise<void>;
455
456
  /**
456
- * Save context messages to a thread without triggering an LLM response.
457
- * Used for injecting system context, file contents, or prior conversation state.
457
+ * Append real transcript messages to a thread **without triggering an LLM
458
+ * response** the write-only half of a turn. The messages join the thread's
459
+ * message history: they persist, surface in {@link getMessages} (in canonical
460
+ * layout, ascending by `createdAt`), and replay to the model as prior
461
+ * conversation on the next {@link stream}. Used to seed a thread with earlier
462
+ * `user` / `assistant` / `tool` turns (e.g. file contents as a user message,
463
+ * or a reconstructed conversation) before the first live prompt.
464
+ *
465
+ * Distinct from {@link setSessionContext}, which writes an out-of-history
466
+ * overlay object that never appears in {@link getMessages}. `addMessages`
467
+ * writes transcript history; `setSessionContext` writes standing background
468
+ * state. `'system'` is not a valid role on the messages here (see
469
+ * {@link MessageRole}); system-level state rides `setSessionContext` or
470
+ * `AgentConfig.instructions`, never the transcript.
458
471
  *
459
472
  * @param agentId - ID of the agent.
460
473
  * @param threadId - ID of the conversation thread.
461
- * @param messages - Messages to add to the thread history.
474
+ * @param messages - Transcript messages to append to the thread history.
462
475
  */
463
- addContext(agentId: string, threadId: string, messages: Message[]): Promise<void>;
476
+ addMessages(agentId: string, threadId: string, messages: Message[]): Promise<void>;
477
+ /**
478
+ * @deprecated Renamed to {@link addMessages} (identical signature/behavior).
479
+ * The name collided semantically with {@link setSessionContext}; the two are
480
+ * distinct channels (transcript history vs. out-of-history overlay). Optional
481
+ * during the deprecation window: harnesses keep it as a thin delegator to
482
+ * {@link addMessages} so existing direct callers keep working, and it will be
483
+ * removed in a follow-up. New harnesses need only implement `addMessages`.
484
+ *
485
+ * @param agentId - ID of the agent.
486
+ * @param threadId - ID of the conversation thread.
487
+ * @param messages - Transcript messages to append to the thread history.
488
+ */
489
+ addContext?(agentId: string, threadId: string, messages: Message[]): Promise<void>;
490
+ /**
491
+ * Replace this thread's session-context object in full (whole-object set,
492
+ * not a merge — merge/delete-key semantics are computed by the caller,
493
+ * typically the service layer's read-merge-write over {@link getSessionContext}).
494
+ *
495
+ * The full contract lands in two stages. Persistence is implemented on every
496
+ * harness in the contract-and-persistence milestone (W-23632685, this PR);
497
+ * model delivery and `compactThread` carry-forward are implemented per
498
+ * harness in a follow-up PR (Mastra W-23632686, Claude W-23632691, OpenAI
499
+ * W-23632694) and are pinned by the seam-gated `runSessionContextConformance`
500
+ * assertions, which register only once a harness supplies its
501
+ * render/carry-forward adapter. The staged bullets below state the contract
502
+ * each harness converges on — not behavior guaranteed live before that
503
+ * harness's follow-up PR lands.
504
+ *
505
+ * Live on every harness this milestone:
506
+ * - Persists per-thread, durably (survives harness restart against the same
507
+ * `storageRootFolder`), with no LLM call.
508
+ * - Does NOT appear in {@link getMessages} output — this channel is disjoint
509
+ * from message history.
510
+ * - Kept separate from the agent's `instructions` — the stored object is an
511
+ * overlay for the turn's system context (once delivery lands), never a
512
+ * mutation of `AgentConfig.instructions` or any shared, cross-thread state.
513
+ * - {@link getSessionContext} returns ONLY the consumer object, and this
514
+ * whole-object set replaces ONLY the consumer's session context. It stays
515
+ * separate from the SDK-owned carried-forward compaction summary (the
516
+ * `previousConversationContext` out-of-history channel that lands later with
517
+ * the History-Continuity subsystem, W-23799514) and MUST NOT clobber it —
518
+ * the two out-of-history channels never share storage. That summary channel
519
+ * does not exist yet, so the "must not clobber" half is pinned by a
520
+ * seam-gated assertion that goes live alongside it.
521
+ *
522
+ * Staged per harness (follow-up PR above; conformance-pinned):
523
+ * - Delivered to the model as system-level context on every subsequent turn
524
+ * on this thread, rendered deterministically (stable key order) so two
525
+ * calls with the same object produce byte-identical rendered text.
526
+ * - Carries forward to the new thread on {@link compactThread}.
527
+ * `compactThread` ROTATES to a new thread (summarize → seed the new
528
+ * thread → best-effort destroy the source), so the source thread does not
529
+ * survive the call. Carry-forward therefore means the object is COPIED
530
+ * from the source onto the new thread DURING compaction, before the
531
+ * source is retired — reading the returned thread yields the same object
532
+ * the source carried.
533
+ *
534
+ * @param agentId - ID of the agent.
535
+ * @param threadId - ID of the conversation thread.
536
+ * @param content - The full replacement session-context object.
537
+ */
538
+ setSessionContext(agentId: string, threadId: string, content: SessionContext): Promise<void>;
539
+ /**
540
+ * Read this thread's current session-context object.
541
+ *
542
+ * Returns `{}` — never `null` or `undefined` — when nothing has been set on
543
+ * this thread yet. An empty and a never-set context are behaviorally
544
+ * identical (both contribute nothing to the rendered prompt), so callers
545
+ * never need a null-check.
546
+ *
547
+ * @param agentId - ID of the agent.
548
+ * @param threadId - ID of the conversation thread.
549
+ * @returns The last object passed to {@link setSessionContext}, or `{}`.
550
+ */
551
+ getSessionContext(agentId: string, threadId: string): Promise<SessionContext>;
464
552
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export type { Message, MessagePart, ImagePart, FilePart } from './types/messages.js';
1
+ export type { Message, MessagePart, MessageRole, ImagePart, FilePart } from './types/messages.js';
2
+ export type { JsonValue, SessionContext } from './types/session-context.js';
2
3
  export type { ChatEvent, StartEvent, TextDeltaEvent, ReasoningDeltaEvent, ToolCallEvent, ToolCallDeltaEvent, ToolApprovalRequestEvent, ToolResultEvent, ToolProgressEvent, StepStartEvent, StepFinishEvent, ErrorEvent, FinishEvent, ChatStreamResult, } from './types/events.js';
3
4
  export type { Decision, ToolDefinition, ToolCallInfo, ToolMatcher, ToolPolicyRule, ToolResultInfo, } from './types/tools.js';
4
5
  export { BUILT_IN_TOOL_POLICIES, SKILL_BRIDGE_SERVER_ID, definePolicy, matcherMatches, resolveToolApprovalPolicy, } from './policy-resolver.js';
@@ -2,12 +2,19 @@ import type { ToolCallInfo, ToolResultInfo } from './tools.js';
2
2
  /**
3
3
  * Role of a message in a conversation. Aligned with AI SDK `ModelMessage` roles.
4
4
  *
5
- * - `system` -- Instructions to the model (not visible to end users)
6
5
  * - `user` -- Messages from the human user
7
6
  * - `assistant` -- Messages generated by the model
8
7
  * - `tool` -- Results from tool executions
8
+ *
9
+ * `'system'` is deliberately NOT a member. System-level state has two dedicated,
10
+ * harness-agnostic channels — static agent instructions (`AgentConfig.instructions`)
11
+ * and per-thread session context (`setSessionContext`) — neither of which flows
12
+ * through the `Message` transcript. `getMessages` never emits a `system` message,
13
+ * and a consumer can never construct one to seed via `addContext` / `POST /messages`.
14
+ * A harness may widen this role set internally for its own runtime, but MUST NOT
15
+ * surface a `system`-role message back across the SDK boundary.
9
16
  */
10
- export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
17
+ export type MessageRole = 'user' | 'assistant' | 'tool';
11
18
  /**
12
19
  * A single message in a conversation thread.
13
20
  *
@@ -27,7 +34,7 @@ export type Message = {
27
34
  /**
28
35
  * Timestamp of when the message was created. **Always populated** on
29
36
  * messages returned from `ChatSession.getMessageHistory()`. **Optional on
30
- * write** — consumers constructing `Message` for `ChatSession.addContext()`
37
+ * write** — consumers constructing `Message` for `ChatSession.addMessages()`
31
38
  * may omit it; the SDK backfills the current time before forwarding to
32
39
  * the harness, so the on-read contract still holds.
33
40
  *
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A JSON-serializable value. Excludes `undefined`, functions, `Date`, class
3
+ * instances, and anything else that doesn't survive `JSON.stringify` /
4
+ * `JSON.parse` unchanged.
5
+ */
6
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
7
+ [key: string]: JsonValue;
8
+ };
9
+ /**
10
+ * A per-thread session-context object: arbitrary consumer-defined keys mapped
11
+ * to JSON-serializable values, delivered to the model as system-level context
12
+ * on every turn and kept durable across harness restart.
13
+ *
14
+ * See {@link AgentHarness.setSessionContext} / {@link AgentHarness.getSessionContext}
15
+ * for the full contract every harness implementation upholds.
16
+ */
17
+ export type SessionContext = {
18
+ [key: string]: JsonValue;
19
+ };
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=session-context.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-sdk",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,9 +47,9 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@eslint/js": "^10.0.1",
50
- "@salesforce/sfdx-agent-harness-claude": "0.45.0",
51
- "@salesforce/sfdx-agent-harness-mastra": "0.48.0",
52
- "@salesforce/sfdx-agent-harness-openai": "0.14.0",
50
+ "@salesforce/sfdx-agent-harness-claude": "0.46.0",
51
+ "@salesforce/sfdx-agent-harness-mastra": "0.49.0",
52
+ "@salesforce/sfdx-agent-harness-openai": "0.15.0",
53
53
  "@types/node": "^22.20.1",
54
54
  "@vitest/coverage-istanbul": "^4.1.10",
55
55
  "@vitest/eslint-plugin": "^1.6.26",