@salesforce/sfdx-agent-sdk 0.80.0 → 0.82.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 +13 -0
- package/README.md +44 -15
- package/dist/agent.d.ts +14 -0
- package/dist/agent.js +46 -9
- package/dist/chat-session.d.ts +125 -4
- package/dist/chat-session.js +221 -15
- package/dist/harness/agent-harness.d.ts +13 -0
- package/dist/harness/harness-config.d.ts +21 -0
- package/dist/harness/harness-config.js +27 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/transcript-occupancy.d.ts +17 -0
- package/dist/internal/transcript-occupancy.js +48 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
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.82.0] - 2026-09-15
|
|
7
|
+
|
|
8
|
+
_No changes — released alongside dependent packages._
|
|
9
|
+
|
|
10
|
+
## [0.81.0] - 2026-09-15
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
- **agent-sdk,harness-claude**: enable auto-compaction by default @W-24153849@ ([#813](https://github.com/forcedotcom/agentic-dx/pull/813))
|
|
14
|
+
|
|
15
|
+
### Chores
|
|
16
|
+
- **deps-dev**: bump eslint from 10.9.1 to 10.10.0 in the eslint group across 1 directory ([#815](https://github.com/forcedotcom/agentic-dx/pull/815))
|
|
17
|
+
- **deps-dev**: bump the dev-dependencies group with 2 updates ([#814](https://github.com/forcedotcom/agentic-dx/pull/814))
|
|
18
|
+
|
|
6
19
|
## [0.80.0] - 2026-09-14
|
|
7
20
|
|
|
8
21
|
### Fixes
|
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ keeps unparameterized call sites working.
|
|
|
154
154
|
| `getChatSessionIds` | `() => string[]` | List active session IDs. |
|
|
155
155
|
| `destroyChatSession` | `(sessionId: string) => Promise<void>` | Destroy a session and its history. |
|
|
156
156
|
| `cloneChatSession` | `(sourceSessionId: string) => Promise<ChatSession>` | Clone a session with its message history. |
|
|
157
|
-
| `compactChatSession` | `(sessionId: string) => Promise<ChatSession>` | Compact a session into a summarized new session.
|
|
157
|
+
| `compactChatSession` | `(sessionId: string) => Promise<ChatSession>` | Compact a session into a summarized **new** session (`session-destroyed` / `session-created` fire; the old `ChatSession` object is detached). Contrast with automatic auto-compaction (see "Auto-Compaction" below), which rotates the **existing** `ChatSession` object's id in place with no lifecycle events. |
|
|
158
158
|
| `destroy` | `() => Promise<void>` | Destroy the agent and all its sessions. |
|
|
159
159
|
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this agent (and its sessions). |
|
|
160
160
|
| `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this agent (and its sessions). |
|
|
@@ -165,7 +165,7 @@ A single conversation thread.
|
|
|
165
165
|
|
|
166
166
|
| Method | Signature | Description |
|
|
167
167
|
| ------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
168
|
-
| `getId` | `() => string` | Session/thread identifier.
|
|
168
|
+
| `getId` | `() => string` | Session/thread identifier. **Not immutable across the session's lifetime:** a successful automatic auto-compaction (see "Auto-Compaction" below) rotates this value in place — same `ChatSession` object, new id. Don't cache the return value as a stable key; call `getId()` again after any `chat()` if you persist it externally. |
|
|
169
169
|
| `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. |
|
|
170
170
|
| `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. |
|
|
171
171
|
| `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. |
|
|
@@ -271,19 +271,20 @@ telemetry for a service of your own.
|
|
|
271
271
|
|
|
272
272
|
#### `AgentConfig`
|
|
273
273
|
|
|
274
|
-
| Field
|
|
275
|
-
|
|
|
276
|
-
| `orgAlias?`
|
|
277
|
-
| `modelId?`
|
|
278
|
-
| `name?`
|
|
279
|
-
| `description?`
|
|
280
|
-
| `instructions?`
|
|
281
|
-
| `tools?`
|
|
282
|
-
| `mcpServers?`
|
|
283
|
-
| `skills?`
|
|
284
|
-
| `rules?`
|
|
285
|
-
| `toolPolicies?`
|
|
286
|
-
| `defaultToolDecision?`
|
|
274
|
+
| Field | Type | Description |
|
|
275
|
+
| -------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
276
|
+
| `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
|
|
277
|
+
| `modelId?` | `ModelName \| Model` | LLM model selector. Pass a `ModelName` enum value for an in-tree model (e.g. `'sfdc_ai__DefaultGPT5'`), or a pre-built `Model` instance to opt into a Bedrock-Anthropic Claude variant the SDK has not yet released — see `createClaudeModel(gatewayId, overrides)` exported from this package. Legacy pinned `llmgateway__*` ids still resolve via a back-compat alias map. |
|
|
278
|
+
| `name?` | `string` | Human-readable agent name. |
|
|
279
|
+
| `description?` | `string` | Agent purpose description. |
|
|
280
|
+
| `instructions?` | `string` | System instructions for the agent. |
|
|
281
|
+
| `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
|
|
282
|
+
| `mcpServers?` | `MCPConfiguration` | MCP server connections. |
|
|
283
|
+
| `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
|
|
284
|
+
| `rules?` | `string[]` | Each entry is either an individual `.md` rule file or a directory of `.md` rule files (scanned one level deep, alphabetical, non-`.md` skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's `.claude/rules/*.md` convention. |
|
|
285
|
+
| `toolPolicies?` | `ToolPolicyRule[]` | Ordered per-tool approval rules resolved by `resolveToolApprovalPolicy` (cross-tier deny-wins / within-tier last-wins). Author directly or via `definePolicy(...)`. See "Tool Approval Policy" below. |
|
|
286
|
+
| `defaultToolDecision?` | `Decision` | Fallback decision when no rule matches. Defaults to `'allow'` (no policy ⇒ no gating). Set to `'require-approval'` for a fail-closed posture (recommended for catalogs with un-annotated MCP servers). |
|
|
287
|
+
| `autoCompactionThreshold?` | `number` | Occupancy ratio (0-1) that triggers a thread compaction before the next turn's model call. Defaults to `DEFAULT_AUTO_COMPACTION_THRESHOLD` (0.9). Auto-compaction is always active — there is no field to disable it. See "Auto-Compaction" below. |
|
|
287
288
|
|
|
288
289
|
#### `AgentStateUpdate` and `ConsumerMetadataMutation`
|
|
289
290
|
|
|
@@ -387,6 +388,34 @@ invariant alone. The exact resolved message reaches both the model and the singl
|
|
|
387
388
|
It is never written to telemetry/log fields and never creates a remembered deny rule; untyped JavaScript that also
|
|
388
389
|
supplies `remember: true` still follows those safety rules.
|
|
389
390
|
|
|
391
|
+
#### Auto-Compaction
|
|
392
|
+
|
|
393
|
+
Every agent auto-compacts its threads once context usage crosses `AgentConfig.autoCompactionThreshold` (default 0.9),
|
|
394
|
+
checked once per turn before the model call. **Auto-compaction is always active — there is no field to disable it.**
|
|
395
|
+
`resolveAutoCompactionThreshold` is the pure resolver harness authors call to apply the default; it's exported for
|
|
396
|
+
consumers who want to inspect the effective threshold for an agent.
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
import { resolveAutoCompactionThreshold, DEFAULT_AUTO_COMPACTION_THRESHOLD } from '@salesforce/sfdx-agent-sdk';
|
|
400
|
+
|
|
401
|
+
resolveAutoCompactionThreshold(config.autoCompactionThreshold);
|
|
402
|
+
// → number
|
|
403
|
+
// defaults to DEFAULT_AUTO_COMPACTION_THRESHOLD (0.9) when omitted
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Raise or lower `autoCompactionThreshold` (0–1) to change how full the context window must be before a compaction is
|
|
407
|
+
triggered. On a harness whose runtime already runs its own native auto-compaction mechanism
|
|
408
|
+
(`AgentHarness.hasNativeAutoCompaction === true`), the SDK's own turn-boundary trigger does not run for that harness's
|
|
409
|
+
sessions — the threshold is forwarded to the harness's native mechanism instead, so double-compaction never happens.
|
|
410
|
+
|
|
411
|
+
**This is a different lifecycle contract than `Agent.compactChatSession()`.** A successful automatic compaction rotates
|
|
412
|
+
the same `ChatSession` object's id **in place** — `session.getId()` returns a new value after the triggering `chat()`
|
|
413
|
+
call, with no `session-destroyed` / `session-created` telemetry. The manual `compactChatSession()` verb instead detaches
|
|
414
|
+
the old session and hands back a brand-new `ChatSession`, firing those lifecycle events. If you key persistence, UI
|
|
415
|
+
state, or logs off `getId()`, re-read it after every `chat()` call rather than caching it — and don't call
|
|
416
|
+
`compactChatSession()` on a thread that already has automatic auto-compaction enabled unless you intend two independent
|
|
417
|
+
compaction paths to potentially run back-to-back.
|
|
418
|
+
|
|
390
419
|
#### `MCPConfiguration`
|
|
391
420
|
|
|
392
421
|
```typescript
|
package/dist/agent.d.ts
CHANGED
|
@@ -383,7 +383,21 @@ export declare class DefaultAgent implements Agent {
|
|
|
383
383
|
* the concrete `DefaultAgent` type rather than `Agent`.
|
|
384
384
|
*/
|
|
385
385
|
restoreSessions(threadIds: string[]): void;
|
|
386
|
+
/** Registers a thread's telemetry-router slice and records its unregister callback. */
|
|
387
|
+
private registerSlice;
|
|
388
|
+
/** Runs and clears a thread's telemetry-router slice unregister callback, if any. */
|
|
389
|
+
private unregisterSlice;
|
|
386
390
|
private attachSession;
|
|
387
391
|
private detachSession;
|
|
392
|
+
/**
|
|
393
|
+
* Re-keys the `sessions` map and the telemetry-router session slice after an in-place
|
|
394
|
+
* auto-compaction rotation (see `DefaultChatSession.maybeAutoCompact`). The session object
|
|
395
|
+
* itself is unchanged — only its backing thread id — so this does NOT emit
|
|
396
|
+
* `session-destroyed` / `session-created`; from the consumer's perspective the same
|
|
397
|
+
* `ChatSession` they already hold just now reports a different `getId()`. Without this,
|
|
398
|
+
* `getChatSession(newThreadId)` would miss and `destroyChatSession(oldThreadId)` would try
|
|
399
|
+
* to destroy a thread the harness already destroyed as part of compaction.
|
|
400
|
+
*/
|
|
401
|
+
private handleAutoCompactionRotation;
|
|
388
402
|
private assertNotDisposed;
|
|
389
403
|
}
|
package/dist/agent.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
5
|
import { LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
6
|
-
import { toHarnessConfig, } from './harness/harness-config.js';
|
|
6
|
+
import { resolveAutoCompactionThreshold, toHarnessConfig, } from './harness/harness-config.js';
|
|
7
7
|
import { normalizeMcpAuthProviders } from './mcp-auth.js';
|
|
8
8
|
import { DefaultChatSession } from './chat-session.js';
|
|
9
9
|
import { AgentSDKError, AgentSDKErrorType } from './errors.js';
|
|
@@ -457,8 +457,22 @@ export class DefaultAgent {
|
|
|
457
457
|
}
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
|
-
|
|
460
|
+
/** Registers a thread's telemetry-router slice and records its unregister callback. */
|
|
461
|
+
registerSlice(threadId) {
|
|
461
462
|
const slice = this.router.registerSession(threadId);
|
|
463
|
+
this.sessionSliceUnregisters.set(threadId, () => this.router.unregisterSession(threadId));
|
|
464
|
+
return slice;
|
|
465
|
+
}
|
|
466
|
+
/** Runs and clears a thread's telemetry-router slice unregister callback, if any. */
|
|
467
|
+
unregisterSlice(threadId) {
|
|
468
|
+
const unregister = this.sessionSliceUnregisters.get(threadId);
|
|
469
|
+
if (unregister) {
|
|
470
|
+
unregister();
|
|
471
|
+
this.sessionSliceUnregisters.delete(threadId);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
attachSession(threadId) {
|
|
475
|
+
const slice = this.registerSlice(threadId);
|
|
462
476
|
// Live getter — read at call time so getContextUsage() reflects the
|
|
463
477
|
// model bound to the agent right now, not the model that was bound
|
|
464
478
|
// when this session was created. updateAgentConfig() can swap the
|
|
@@ -476,12 +490,20 @@ export class DefaultAgent {
|
|
|
476
490
|
const persistRememberedRule = async (rule) => {
|
|
477
491
|
await this.updateAgentConfig({ toolPolicies: [...(this.config.toolPolicies ?? []), rule] });
|
|
478
492
|
};
|
|
493
|
+
// Live getter, same pattern as `getContextWindow` — reads `this.config.autoCompactionThreshold`
|
|
494
|
+
// at call time so an `updateAgentConfig()` change takes effect on the session's next turn.
|
|
495
|
+
const getAutoCompactionThreshold = () => resolveAutoCompactionThreshold(this.config.autoCompactionThreshold);
|
|
479
496
|
const session = new DefaultChatSession(this.harness, this.agentId, threadId, slice, {
|
|
480
497
|
telemetry: this.telemetryBus,
|
|
481
498
|
log: this.logBus,
|
|
482
|
-
}, getContextWindow, {
|
|
499
|
+
}, getContextWindow, {
|
|
500
|
+
clock: this.clock,
|
|
501
|
+
idGenerator: this.idGenerator,
|
|
502
|
+
persistRememberedRule,
|
|
503
|
+
getAutoCompactionThreshold,
|
|
504
|
+
onThreadIdRotated: (oldThreadId, newThreadId) => this.handleAutoCompactionRotation(oldThreadId, newThreadId),
|
|
505
|
+
});
|
|
483
506
|
this.sessions.set(threadId, session);
|
|
484
|
-
this.sessionSliceUnregisters.set(threadId, () => this.router.unregisterSession(threadId));
|
|
485
507
|
const sessionCreatedAt = this.clock.now();
|
|
486
508
|
// Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
|
|
487
509
|
this.telemetryBus.emitTelemetry({ type: 'session-created', agentId: this.agentId, threadId }, sessionCreatedAt);
|
|
@@ -503,13 +525,28 @@ export class DefaultAgent {
|
|
|
503
525
|
else {
|
|
504
526
|
session.releaseWithoutEvent();
|
|
505
527
|
}
|
|
506
|
-
|
|
507
|
-
if (unregister) {
|
|
508
|
-
unregister();
|
|
509
|
-
this.sessionSliceUnregisters.delete(threadId);
|
|
510
|
-
}
|
|
528
|
+
this.unregisterSlice(threadId);
|
|
511
529
|
this.sessions.delete(threadId);
|
|
512
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Re-keys the `sessions` map and the telemetry-router session slice after an in-place
|
|
533
|
+
* auto-compaction rotation (see `DefaultChatSession.maybeAutoCompact`). The session object
|
|
534
|
+
* itself is unchanged — only its backing thread id — so this does NOT emit
|
|
535
|
+
* `session-destroyed` / `session-created`; from the consumer's perspective the same
|
|
536
|
+
* `ChatSession` they already hold just now reports a different `getId()`. Without this,
|
|
537
|
+
* `getChatSession(newThreadId)` would miss and `destroyChatSession(oldThreadId)` would try
|
|
538
|
+
* to destroy a thread the harness already destroyed as part of compaction.
|
|
539
|
+
*/
|
|
540
|
+
handleAutoCompactionRotation(oldThreadId, newThreadId) {
|
|
541
|
+
const session = this.sessions.get(oldThreadId);
|
|
542
|
+
if (!session)
|
|
543
|
+
return;
|
|
544
|
+
this.sessions.delete(oldThreadId);
|
|
545
|
+
this.sessions.set(newThreadId, session);
|
|
546
|
+
this.unregisterSlice(oldThreadId);
|
|
547
|
+
const newSlice = this.registerSlice(newThreadId);
|
|
548
|
+
session.rebindInboundSlice(newSlice);
|
|
549
|
+
}
|
|
513
550
|
assertNotDisposed() {
|
|
514
551
|
if (this.disposed) {
|
|
515
552
|
throw new AgentSDKError('Agent has been disposed.', AgentSDKErrorType.DISPOSED);
|
package/dist/chat-session.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Clock, LogBus, type LogRecord, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
|
|
2
2
|
import type { AgentHarness } from './harness/agent-harness.js';
|
|
3
|
-
import type
|
|
3
|
+
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';
|
|
@@ -21,6 +21,16 @@ export type ChatOptions = StreamOptions;
|
|
|
21
21
|
* `toolPolicies`-only partial so no connectivity re-resolution is triggered.
|
|
22
22
|
*/
|
|
23
23
|
export type RememberedRulePersister = (rule: ToolPolicyRule) => Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Notifies the owning `Agent` that a session's backing thread was rotated by an
|
|
26
|
+
* in-place auto-compaction (the session kept the same object identity, but
|
|
27
|
+
* {@link ChatSession.getId} now returns a new value). Supplied by `DefaultAgent`
|
|
28
|
+
* so it can re-key its `sessions` map and the telemetry-router session slice from
|
|
29
|
+
* the old id to the new one — otherwise `Agent.getChatSession` /
|
|
30
|
+
* `destroyChatSession` would keep looking up a thread the harness already
|
|
31
|
+
* destroyed. Omitted in unit tests that don't need this bookkeeping.
|
|
32
|
+
*/
|
|
33
|
+
export type ThreadIdRotatedNotifier = (oldThreadId: string, newThreadId: string) => void;
|
|
24
34
|
/**
|
|
25
35
|
* Optional injected dependencies for a {@link DefaultChatSession}. Bundled into
|
|
26
36
|
* one trailing bag (rather than a growing tail of positional params) so a new
|
|
@@ -41,6 +51,16 @@ export type ChatSessionDeps = {
|
|
|
41
51
|
* then behaves as a one-shot with no write. See {@link RememberedRulePersister}.
|
|
42
52
|
*/
|
|
43
53
|
persistRememberedRule?: RememberedRulePersister;
|
|
54
|
+
/**
|
|
55
|
+
* Live getter for the agent's resolved auto-compaction threshold. Called at the
|
|
56
|
+
* start of every {@link ChatSession.chat} so a live `Agent.updateAgentConfig()` change
|
|
57
|
+
* to `AgentConfig.autoCompactionThreshold` takes effect on the next turn — same pattern
|
|
58
|
+
* as `getContextWindow`. Defaults to `DEFAULT_AUTO_COMPACTION_THRESHOLD` (0.9) when
|
|
59
|
+
* omitted.
|
|
60
|
+
*/
|
|
61
|
+
getAutoCompactionThreshold?: () => number;
|
|
62
|
+
/** See {@link ThreadIdRotatedNotifier}. */
|
|
63
|
+
onThreadIdRotated?: ThreadIdRotatedNotifier;
|
|
44
64
|
};
|
|
45
65
|
/**
|
|
46
66
|
* Parent bus pair used to wire upward forwarding at construction time.
|
|
@@ -321,7 +341,14 @@ export interface ChatSession {
|
|
|
321
341
|
export declare class DefaultChatSession implements ChatSession {
|
|
322
342
|
private readonly harness;
|
|
323
343
|
private readonly agentId;
|
|
324
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Mutable (not `readonly`): auto-compaction rotates a session's backing thread in
|
|
346
|
+
* place — see {@link maybeAutoCompact} — so the session's public id
|
|
347
|
+
* ({@link getId}) can change over its lifetime while the object identity stays
|
|
348
|
+
* the same. {@link onThreadIdRotated} notifies `DefaultAgent` so its `sessions`
|
|
349
|
+
* map and telemetry-router slice stay keyed correctly.
|
|
350
|
+
*/
|
|
351
|
+
private threadId;
|
|
325
352
|
private readonly chatEventBus;
|
|
326
353
|
private readonly telemetryBus;
|
|
327
354
|
private readonly logBus;
|
|
@@ -390,6 +417,18 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
390
417
|
*/
|
|
391
418
|
private latestTurnUsage;
|
|
392
419
|
private latestStepCount;
|
|
420
|
+
/**
|
|
421
|
+
* Whether {@link latestUsage} reflects the last COMPLETED normal turn (`true`) or is stale —
|
|
422
|
+
* unset (fresh session), or left over from an errored/abandoned turn (`false`). Read by
|
|
423
|
+
* {@link currentOccupancy} to decide between the two occupancy signals: a trustworthy live
|
|
424
|
+
* reading is preferred; otherwise occupancy falls back to a transcript-size estimate. Set
|
|
425
|
+
* `true` in {@link wrapEventStream}'s natural-completion branch, `false` in its error branch
|
|
426
|
+
* and by {@link clearHistory}.
|
|
427
|
+
*/
|
|
428
|
+
private lastTurnUsageTrustworthy;
|
|
429
|
+
/** Live getter for the agent's resolved auto-compaction threshold. See {@link ChatSessionDeps.getAutoCompactionThreshold}. */
|
|
430
|
+
private readonly getAutoCompactionThreshold;
|
|
431
|
+
private readonly onThreadIdRotated;
|
|
393
432
|
private disposed;
|
|
394
433
|
/**
|
|
395
434
|
* True while a turn started by {@link chat} is in flight — from the `chat()`
|
|
@@ -443,6 +482,9 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
443
482
|
* - MUST return an object containing the original `textStream` and the wrapped `eventStream`.
|
|
444
483
|
* - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
|
|
445
484
|
* before returning a stream result.
|
|
485
|
+
* - MUST check the auto-compaction policy before the harness call and, if the current
|
|
486
|
+
* occupancy is at/above its threshold, compact the thread first (see
|
|
487
|
+
* {@link maybeAutoCompact}) so the turn proceeds against the (now smaller) thread.
|
|
446
488
|
*/
|
|
447
489
|
chat(message: string | MessagePart[], options?: ChatOptions): Promise<ChatStreamResult>;
|
|
448
490
|
/**
|
|
@@ -492,7 +534,10 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
492
534
|
* `chat-stream-started` is emitted by the entry-point method (chat / submitToolResult /
|
|
493
535
|
* approveToolCall / declineToolCall) before the harness call so that pre-stream rejections
|
|
494
536
|
* still produce a started+error pair. `startedAt` is captured there and threaded down so
|
|
495
|
-
* `durationMs` measures real elapsed time on both terminal events.
|
|
537
|
+
* `durationMs` measures real elapsed time on both terminal events. `threadId` is likewise
|
|
538
|
+
* captured before the harness call (pre-auto-compaction-rotation) and threaded down so the
|
|
539
|
+
* `chat-stream-started`/`chat-stream-completed`/`chat-stream-error` triple for one turn stays
|
|
540
|
+
* paired on the same threadId even when `maybeAutoCompact()` rotates `this.threadId` mid-turn.
|
|
496
541
|
*/
|
|
497
542
|
private wrapEventStream;
|
|
498
543
|
/**
|
|
@@ -547,6 +592,8 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
547
592
|
* absent) until the next turn produces one.
|
|
548
593
|
*/
|
|
549
594
|
clearHistory(): Promise<void>;
|
|
595
|
+
/** Drops all retained per-turn usage state — shared by {@link clearHistory} and a successful {@link maybeAutoCompact} rotation, both of which retire the thread the retained state described. */
|
|
596
|
+
private resetUsageState;
|
|
550
597
|
/**
|
|
551
598
|
* @requirements
|
|
552
599
|
* - MUST always return a populated `ContextUsage`. Pre-first-turn and post-`clearHistory()`,
|
|
@@ -596,6 +643,77 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
596
643
|
* read from the same `latestUsage` snapshot `wrapEventStream` captures.
|
|
597
644
|
*/
|
|
598
645
|
private contextUsageLogFields;
|
|
646
|
+
/**
|
|
647
|
+
* Pre-turn occupancy hook, called from {@link chat} before the harness call. Auto-compaction
|
|
648
|
+
* is always active — there is no "disabled" state to check. This method short-circuits
|
|
649
|
+
* immediately when `this.harness.hasNativeAutoCompaction` is `true`: a harness that runs its
|
|
650
|
+
* own native auto-compaction mechanism must not ALSO have the SDK's turn-boundary trigger run
|
|
651
|
+
* against the same conversation — that would double-compact via two uncoordinated mechanisms.
|
|
652
|
+
* A harness making that claim is expected to honor `AgentConfig.autoCompactionThreshold`
|
|
653
|
+
* itself, by forwarding it to its native mechanism; the SDK never verifies this.
|
|
654
|
+
*
|
|
655
|
+
* Otherwise, when {@link currentOccupancy} is at/above the resolved
|
|
656
|
+
* {@link ChatSessionDeps.getAutoCompactionThreshold} threshold, compacts the thread in place —
|
|
657
|
+
* the session keeps its object identity but {@link getId} returns a new value afterward, and
|
|
658
|
+
* {@link onThreadIdRotated} notifies `DefaultAgent` to re-key its bookkeeping. Both outcomes
|
|
659
|
+
* (`'compacted'` and `'failed'`) emit a structured `logBus` record tagged
|
|
660
|
+
* `event_type: 'auto-compaction'` — this log has no colocated telemetry sibling, unlike the
|
|
661
|
+
* Tier-1 sites documented in `ARCHITECTURE.md`, so a subscriber must read it off `logBus`
|
|
662
|
+
* directly.
|
|
663
|
+
*
|
|
664
|
+
* A `COMPACTION_FAILED` error is swallowed: the session is left intact (still pointed at
|
|
665
|
+
* the pre-compaction thread, per the harness contract's "leave it intact on failure"
|
|
666
|
+
* clause) and the turn proceeds normally against it rather than failing the whole `chat()`
|
|
667
|
+
* call over a best-effort maintenance operation. Any other error from {@link currentOccupancy}
|
|
668
|
+
* (e.g. a transient `getMessages()` failure while estimating transcript occupancy) is
|
|
669
|
+
* likewise swallowed (after a `logBus.warn`) — it is a best-effort heuristic read, not the
|
|
670
|
+
* actual harness operation, and a hiccup there must not fail the user's turn. Any other error
|
|
671
|
+
* from `harness.compactThread` propagates to `chat()`'s existing pre-stream-failure handling.
|
|
672
|
+
*
|
|
673
|
+
* `abortSignal` is `chat()`'s composed signal — `cancelTurn()`'s own per-turn controller
|
|
674
|
+
* folded together with any caller-supplied `options.abortSignal` — checked before starting
|
|
675
|
+
* the occupancy read and again before calling `compactThread`, so an abort from EITHER source
|
|
676
|
+
* that lands while this is running skips (rather than starts) compaction. This narrows, but
|
|
677
|
+
* does not close, the window an abort can block on: `AgentHarness.compactThread` takes no
|
|
678
|
+
* `AbortSignal`, so a `compactThread` call already in flight when the signal aborts cannot be
|
|
679
|
+
* interrupted — the caller blocks for the remaining compaction latency, and if
|
|
680
|
+
* `harness.stream()` subsequently runs against an already-aborted signal, the turn can end via
|
|
681
|
+
* `notifyPreStreamError` with `finishReason: 'error'` rather than `'cancelled'`. See
|
|
682
|
+
* `cancelTurn()`'s Critical Invariant below for the full contract and why this residual gap is
|
|
683
|
+
* accepted for now.
|
|
684
|
+
*/
|
|
685
|
+
private maybeAutoCompact;
|
|
686
|
+
/**
|
|
687
|
+
* The occupancy ratio (0-1) `maybeAutoCompact` checks against the configured threshold,
|
|
688
|
+
* from whichever of two signals is available, in priority order:
|
|
689
|
+
*
|
|
690
|
+
* 1. **Live usage** — when {@link lastTurnUsageTrustworthy}, the same `effectiveInputTokens()
|
|
691
|
+
* / contextWindow` computation {@link getContextUsage} exposes as `usedFraction`.
|
|
692
|
+
* 2. **Transcript-size estimate** — otherwise (cold session, or the prior turn ended
|
|
693
|
+
* abnormally), a rough estimate from the persisted transcript's character count via
|
|
694
|
+
* {@link estimateTranscriptOccupancy}.
|
|
695
|
+
*
|
|
696
|
+
* Returns `undefined` when neither signal is available (a brand-new session with no
|
|
697
|
+
* completed turn and no persisted messages), so `maybeAutoCompact` never compacts a
|
|
698
|
+
* session with nothing to compact.
|
|
699
|
+
*/
|
|
700
|
+
private currentOccupancy;
|
|
701
|
+
/**
|
|
702
|
+
* Estimates occupancy from the thread's persisted transcript size (chars ÷
|
|
703
|
+
* {@link CHARS_PER_TOKEN} as a token-count proxy) when no trustworthy live usage reading is
|
|
704
|
+
* available. Returns `undefined` for an empty transcript so a brand-new thread never
|
|
705
|
+
* triggers compaction.
|
|
706
|
+
*/
|
|
707
|
+
private estimateTranscriptOccupancy;
|
|
708
|
+
/**
|
|
709
|
+
* Re-points this session's inbound router-slice forwarding at a new slice, unsubscribing
|
|
710
|
+
* the old one first. Called by `DefaultAgent` after an auto-compaction rotation re-registers
|
|
711
|
+
* the telemetry-router slice under the new thread id, so any future thread-scoped
|
|
712
|
+
* harness-emitted event routes to this session under its new id rather than falling through
|
|
713
|
+
* to "unrouted". Not part of the public {@link ChatSession} interface — `DefaultAgent` holds
|
|
714
|
+
* the concrete `DefaultChatSession` type, same as {@link releaseWithoutEvent}.
|
|
715
|
+
*/
|
|
716
|
+
rebindInboundSlice(slice: TelemetrySlice): void;
|
|
599
717
|
/**
|
|
600
718
|
* @requirements
|
|
601
719
|
* - IF `message` is a `string`, it MUST be formatted into a standard `Message` object array containing exactly one message.
|
|
@@ -699,7 +817,10 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
699
817
|
* not throw.
|
|
700
818
|
*
|
|
701
819
|
* `startedAt` is the timestamp captured by {@link emitChatStreamStarted} so `durationMs`
|
|
702
|
-
* measures real elapsed time even for pre-stream rejections.
|
|
820
|
+
* measures real elapsed time even for pre-stream rejections. `threadId` is the same
|
|
821
|
+
* pre-auto-compaction-rotation id captured alongside `startedAt`, so this event stays paired
|
|
822
|
+
* with its `chat-stream-started` even when the rejection surfaces after a mid-turn rotation
|
|
823
|
+
* (e.g. `harness.stream()` itself fails on the post-rotation thread).
|
|
703
824
|
*/
|
|
704
825
|
private notifyPreStreamError;
|
|
705
826
|
/**
|
package/dist/chat-session.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
5
|
import { backfillCreatedAt, EventBus, LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
6
|
+
import { resolveAutoCompactionThreshold } from './harness/harness-config.js';
|
|
6
7
|
import { resolveToolDeclineModelMessage } from './harness/tool-decline.js';
|
|
7
8
|
import { AgentSDKError, AgentSDKErrorType } from './errors.js';
|
|
9
|
+
import { CHARS_PER_TOKEN, estimateTranscriptChars } from './internal/transcript-occupancy.js';
|
|
8
10
|
import { createTelemetryBus, } from './types/telemetry-events.js';
|
|
9
11
|
/**
|
|
10
12
|
* Composes the SDK-owned per-turn abort signal with the caller's (if any) into
|
|
@@ -16,6 +18,10 @@ import { createTelemetryBus, } from './types/telemetry-events.js';
|
|
|
16
18
|
function composeAbortSignals(own, caller) {
|
|
17
19
|
return caller === undefined ? own : AbortSignal.any([own, caller]);
|
|
18
20
|
}
|
|
21
|
+
/** Clamps an occupancy ratio to `[0, 1]` — a denominator/numerator pair can fall outside that range (e.g. a `contextWindow` change mid-session, or an estimate overshoot). */
|
|
22
|
+
function clampFraction(numerator, denominator) {
|
|
23
|
+
return Math.min(1, Math.max(0, numerator / denominator));
|
|
24
|
+
}
|
|
19
25
|
/**
|
|
20
26
|
* Default implementation of {@link ChatSession} that delegates all operations
|
|
21
27
|
* to an {@link AgentHarness}. The session holds its agent ID and thread ID
|
|
@@ -24,6 +30,13 @@ function composeAbortSignals(own, caller) {
|
|
|
24
30
|
export class DefaultChatSession {
|
|
25
31
|
harness;
|
|
26
32
|
agentId;
|
|
33
|
+
/**
|
|
34
|
+
* Mutable (not `readonly`): auto-compaction rotates a session's backing thread in
|
|
35
|
+
* place — see {@link maybeAutoCompact} — so the session's public id
|
|
36
|
+
* ({@link getId}) can change over its lifetime while the object identity stays
|
|
37
|
+
* the same. {@link onThreadIdRotated} notifies `DefaultAgent` so its `sessions`
|
|
38
|
+
* map and telemetry-router slice stay keyed correctly.
|
|
39
|
+
*/
|
|
27
40
|
threadId;
|
|
28
41
|
chatEventBus = new EventBus();
|
|
29
42
|
// Constructed in the constructor body so they use the injected `clock` (tests inject a StubClock for
|
|
@@ -95,6 +108,18 @@ export class DefaultChatSession {
|
|
|
95
108
|
*/
|
|
96
109
|
latestTurnUsage = undefined;
|
|
97
110
|
latestStepCount = 0;
|
|
111
|
+
/**
|
|
112
|
+
* Whether {@link latestUsage} reflects the last COMPLETED normal turn (`true`) or is stale —
|
|
113
|
+
* unset (fresh session), or left over from an errored/abandoned turn (`false`). Read by
|
|
114
|
+
* {@link currentOccupancy} to decide between the two occupancy signals: a trustworthy live
|
|
115
|
+
* reading is preferred; otherwise occupancy falls back to a transcript-size estimate. Set
|
|
116
|
+
* `true` in {@link wrapEventStream}'s natural-completion branch, `false` in its error branch
|
|
117
|
+
* and by {@link clearHistory}.
|
|
118
|
+
*/
|
|
119
|
+
lastTurnUsageTrustworthy = false;
|
|
120
|
+
/** Live getter for the agent's resolved auto-compaction threshold. See {@link ChatSessionDeps.getAutoCompactionThreshold}. */
|
|
121
|
+
getAutoCompactionThreshold;
|
|
122
|
+
onThreadIdRotated;
|
|
98
123
|
disposed = false;
|
|
99
124
|
/**
|
|
100
125
|
* True while a turn started by {@link chat} is in flight — from the `chat()`
|
|
@@ -146,6 +171,9 @@ export class DefaultChatSession {
|
|
|
146
171
|
this.clock = deps.clock ?? new RealClock();
|
|
147
172
|
this.idGenerator = deps.idGenerator ?? new UUIDGenerator();
|
|
148
173
|
this.persistRememberedRule = deps.persistRememberedRule;
|
|
174
|
+
this.getAutoCompactionThreshold =
|
|
175
|
+
deps.getAutoCompactionThreshold ?? (() => resolveAutoCompactionThreshold(undefined));
|
|
176
|
+
this.onThreadIdRotated = deps.onThreadIdRotated;
|
|
149
177
|
this.telemetryBus = createTelemetryBus(this.clock);
|
|
150
178
|
this.logBus = new LogBus(this.clock);
|
|
151
179
|
this.inboundUnsubs = [inbound.telemetry.forwardTo(this.telemetryBus), inbound.log.forwardTo(this.logBus)];
|
|
@@ -163,6 +191,9 @@ export class DefaultChatSession {
|
|
|
163
191
|
* - MUST return an object containing the original `textStream` and the wrapped `eventStream`.
|
|
164
192
|
* - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
|
|
165
193
|
* before returning a stream result.
|
|
194
|
+
* - MUST check the auto-compaction policy before the harness call and, if the current
|
|
195
|
+
* occupancy is at/above its threshold, compact the thread first (see
|
|
196
|
+
* {@link maybeAutoCompact}) so the turn proceeds against the (now smaller) thread.
|
|
166
197
|
*/
|
|
167
198
|
async chat(message, options) {
|
|
168
199
|
this.assertNotDisposed();
|
|
@@ -181,7 +212,17 @@ export class DefaultChatSession {
|
|
|
181
212
|
this.turnAbortController = turnAbort;
|
|
182
213
|
const abortSignal = composeAbortSignals(turnAbort.signal, options?.abortSignal);
|
|
183
214
|
const startedAt = this.emitChatStreamStarted('chat');
|
|
184
|
-
|
|
215
|
+
// Captured before `maybeAutoCompact()` runs so the whole `chat-stream-started`
|
|
216
|
+
// / `chat-stream-completed` / `chat-stream-error` triple for THIS turn carries
|
|
217
|
+
// the same threadId, even though auto-compaction may rotate `this.threadId` in
|
|
218
|
+
// place mid-turn (the telemetry pairing contract keys on the pre-rotation id —
|
|
219
|
+
// the thread the turn was issued against).
|
|
220
|
+
const turnThreadId = this.threadId;
|
|
221
|
+
// Chained (not `await`ed here) so the auto-compaction check runs to completion
|
|
222
|
+
// before `harness.stream()` is invoked — it may rotate `this.threadId` in place,
|
|
223
|
+
// and the stream call must see the post-rotation id — while `streamPromise` is
|
|
224
|
+
// still derived synchronously, before any `await` in this function body.
|
|
225
|
+
const streamPromise = this.maybeAutoCompact(abortSignal).then(() => this.harness.stream(this.agentId, this.threadId, message, { ...options, abortSignal }));
|
|
185
226
|
// Derive the settle barrier synchronously from the stream promise — before
|
|
186
227
|
// the `await` — so a `cancelTurn()` that races this `chat()` still awaits the
|
|
187
228
|
// real teardown + persistence flush rather than an `undefined` (an immediate
|
|
@@ -193,14 +234,14 @@ export class DefaultChatSession {
|
|
|
193
234
|
const result = await streamPromise;
|
|
194
235
|
return {
|
|
195
236
|
textStream: result.textStream,
|
|
196
|
-
eventStream: this.wrapEventStream(result.eventStream, startedAt),
|
|
237
|
+
eventStream: this.wrapEventStream(result.eventStream, startedAt, turnThreadId),
|
|
197
238
|
};
|
|
198
239
|
}
|
|
199
240
|
catch (err) {
|
|
200
241
|
// Pre-stream failure ends the turn before a stream exists — clear the
|
|
201
242
|
// flag here since `wrapEventStream` (which normally clears it) never runs.
|
|
202
243
|
this.turnActive = false;
|
|
203
|
-
this.notifyPreStreamError(err, startedAt);
|
|
244
|
+
this.notifyPreStreamError(err, startedAt, turnThreadId);
|
|
204
245
|
throw err;
|
|
205
246
|
}
|
|
206
247
|
}
|
|
@@ -277,9 +318,12 @@ export class DefaultChatSession {
|
|
|
277
318
|
* `chat-stream-started` is emitted by the entry-point method (chat / submitToolResult /
|
|
278
319
|
* approveToolCall / declineToolCall) before the harness call so that pre-stream rejections
|
|
279
320
|
* still produce a started+error pair. `startedAt` is captured there and threaded down so
|
|
280
|
-
* `durationMs` measures real elapsed time on both terminal events.
|
|
321
|
+
* `durationMs` measures real elapsed time on both terminal events. `threadId` is likewise
|
|
322
|
+
* captured before the harness call (pre-auto-compaction-rotation) and threaded down so the
|
|
323
|
+
* `chat-stream-started`/`chat-stream-completed`/`chat-stream-error` triple for one turn stays
|
|
324
|
+
* paired on the same threadId even when `maybeAutoCompact()` rotates `this.threadId` mid-turn.
|
|
281
325
|
*/
|
|
282
|
-
async *wrapEventStream(stream, startedAt) {
|
|
326
|
+
async *wrapEventStream(stream, startedAt, threadId) {
|
|
283
327
|
let sawFinish = false;
|
|
284
328
|
let lastError;
|
|
285
329
|
let finishUsage;
|
|
@@ -371,11 +415,15 @@ export class DefaultChatSession {
|
|
|
371
415
|
const finishedAt = this.clock.now();
|
|
372
416
|
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
373
417
|
if (lastError !== undefined) {
|
|
418
|
+
// The retained `latestUsage` is stale for occupancy purposes — the errored/abandoned
|
|
419
|
+
// turn may have jumped occupancy without a completed step-finish reflecting it. The
|
|
420
|
+
// next `maybeAutoCompact()` falls back to the transcript-size estimate instead.
|
|
421
|
+
this.lastTurnUsageTrustworthy = false;
|
|
374
422
|
// Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
|
|
375
423
|
this.telemetryBus.emitTelemetry({
|
|
376
424
|
type: 'chat-stream-error',
|
|
377
425
|
agentId: this.agentId,
|
|
378
|
-
threadId
|
|
426
|
+
threadId,
|
|
379
427
|
durationMs,
|
|
380
428
|
error: lastError,
|
|
381
429
|
}, finishedAt);
|
|
@@ -385,7 +433,7 @@ export class DefaultChatSession {
|
|
|
385
433
|
context: {
|
|
386
434
|
event_type: 'chat-stream-error',
|
|
387
435
|
agentId: this.agentId,
|
|
388
|
-
threadId
|
|
436
|
+
threadId,
|
|
389
437
|
durationMs,
|
|
390
438
|
},
|
|
391
439
|
error: lastError,
|
|
@@ -404,10 +452,13 @@ export class DefaultChatSession {
|
|
|
404
452
|
// `getContextUsage()` (which spreads on read, carrying the mutation).
|
|
405
453
|
this.latestTurnUsage = finishUsage === undefined ? undefined : { ...finishUsage };
|
|
406
454
|
this.latestStepCount = stepCount;
|
|
455
|
+
// A completed turn's `latestUsage` is a trustworthy live occupancy reading —
|
|
456
|
+
// `maybeAutoCompact()` on the next `chat()` can use it directly.
|
|
457
|
+
this.lastTurnUsageTrustworthy = true;
|
|
407
458
|
this.telemetryBus.emitTelemetry({
|
|
408
459
|
type: 'chat-stream-completed',
|
|
409
460
|
agentId: this.agentId,
|
|
410
|
-
threadId
|
|
461
|
+
threadId,
|
|
411
462
|
durationMs,
|
|
412
463
|
// Self-describing usage on telemetry (W-24125085, Phase 2): `usage`
|
|
413
464
|
// stays as the legacy alias for the whole-turn aggregate, now also
|
|
@@ -451,7 +502,7 @@ export class DefaultChatSession {
|
|
|
451
502
|
context: {
|
|
452
503
|
event_type: 'chat-stream-completed',
|
|
453
504
|
agentId: this.agentId,
|
|
454
|
-
threadId
|
|
505
|
+
threadId,
|
|
455
506
|
durationMs,
|
|
456
507
|
...(finishUsage !== undefined ? { turnUsage: finishUsage } : {}),
|
|
457
508
|
...(stepCount > 0 ? { stepCount } : {}),
|
|
@@ -548,9 +599,14 @@ export class DefaultChatSession {
|
|
|
548
599
|
async clearHistory() {
|
|
549
600
|
this.assertNotDisposed();
|
|
550
601
|
await this.harness.clearMessages(this.agentId, this.threadId);
|
|
602
|
+
this.resetUsageState();
|
|
603
|
+
}
|
|
604
|
+
/** Drops all retained per-turn usage state — shared by {@link clearHistory} and a successful {@link maybeAutoCompact} rotation, both of which retire the thread the retained state described. */
|
|
605
|
+
resetUsageState() {
|
|
551
606
|
this.latestUsage = {};
|
|
552
607
|
this.latestTurnUsage = undefined;
|
|
553
608
|
this.latestStepCount = 0;
|
|
609
|
+
this.lastTurnUsageTrustworthy = false;
|
|
554
610
|
}
|
|
555
611
|
/**
|
|
556
612
|
* @requirements
|
|
@@ -579,7 +635,7 @@ export class DefaultChatSession {
|
|
|
579
635
|
this.assertNotDisposed();
|
|
580
636
|
const contextWindow = this.getContextWindow();
|
|
581
637
|
const contextTokens = this.effectiveInputTokens();
|
|
582
|
-
const usedFraction = contextTokens === undefined ? undefined :
|
|
638
|
+
const usedFraction = contextTokens === undefined ? undefined : clampFraction(contextTokens, contextWindow);
|
|
583
639
|
// Spread `latestUsage` so consumer mutation of the returned object cannot
|
|
584
640
|
// leak back into the session's retained state on a later call. `usage` is
|
|
585
641
|
// the legacy alias for `lastStepUsage` (Phase 2, W-24125085) — the SAME
|
|
@@ -642,9 +698,156 @@ export class DefaultChatSession {
|
|
|
642
698
|
lastStepUsage: { ...this.latestUsage },
|
|
643
699
|
contextTokens,
|
|
644
700
|
contextWindow,
|
|
645
|
-
usedFraction:
|
|
701
|
+
usedFraction: clampFraction(contextTokens, contextWindow),
|
|
646
702
|
};
|
|
647
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Pre-turn occupancy hook, called from {@link chat} before the harness call. Auto-compaction
|
|
706
|
+
* is always active — there is no "disabled" state to check. This method short-circuits
|
|
707
|
+
* immediately when `this.harness.hasNativeAutoCompaction` is `true`: a harness that runs its
|
|
708
|
+
* own native auto-compaction mechanism must not ALSO have the SDK's turn-boundary trigger run
|
|
709
|
+
* against the same conversation — that would double-compact via two uncoordinated mechanisms.
|
|
710
|
+
* A harness making that claim is expected to honor `AgentConfig.autoCompactionThreshold`
|
|
711
|
+
* itself, by forwarding it to its native mechanism; the SDK never verifies this.
|
|
712
|
+
*
|
|
713
|
+
* Otherwise, when {@link currentOccupancy} is at/above the resolved
|
|
714
|
+
* {@link ChatSessionDeps.getAutoCompactionThreshold} threshold, compacts the thread in place —
|
|
715
|
+
* the session keeps its object identity but {@link getId} returns a new value afterward, and
|
|
716
|
+
* {@link onThreadIdRotated} notifies `DefaultAgent` to re-key its bookkeeping. Both outcomes
|
|
717
|
+
* (`'compacted'` and `'failed'`) emit a structured `logBus` record tagged
|
|
718
|
+
* `event_type: 'auto-compaction'` — this log has no colocated telemetry sibling, unlike the
|
|
719
|
+
* Tier-1 sites documented in `ARCHITECTURE.md`, so a subscriber must read it off `logBus`
|
|
720
|
+
* directly.
|
|
721
|
+
*
|
|
722
|
+
* A `COMPACTION_FAILED` error is swallowed: the session is left intact (still pointed at
|
|
723
|
+
* the pre-compaction thread, per the harness contract's "leave it intact on failure"
|
|
724
|
+
* clause) and the turn proceeds normally against it rather than failing the whole `chat()`
|
|
725
|
+
* call over a best-effort maintenance operation. Any other error from {@link currentOccupancy}
|
|
726
|
+
* (e.g. a transient `getMessages()` failure while estimating transcript occupancy) is
|
|
727
|
+
* likewise swallowed (after a `logBus.warn`) — it is a best-effort heuristic read, not the
|
|
728
|
+
* actual harness operation, and a hiccup there must not fail the user's turn. Any other error
|
|
729
|
+
* from `harness.compactThread` propagates to `chat()`'s existing pre-stream-failure handling.
|
|
730
|
+
*
|
|
731
|
+
* `abortSignal` is `chat()`'s composed signal — `cancelTurn()`'s own per-turn controller
|
|
732
|
+
* folded together with any caller-supplied `options.abortSignal` — checked before starting
|
|
733
|
+
* the occupancy read and again before calling `compactThread`, so an abort from EITHER source
|
|
734
|
+
* that lands while this is running skips (rather than starts) compaction. This narrows, but
|
|
735
|
+
* does not close, the window an abort can block on: `AgentHarness.compactThread` takes no
|
|
736
|
+
* `AbortSignal`, so a `compactThread` call already in flight when the signal aborts cannot be
|
|
737
|
+
* interrupted — the caller blocks for the remaining compaction latency, and if
|
|
738
|
+
* `harness.stream()` subsequently runs against an already-aborted signal, the turn can end via
|
|
739
|
+
* `notifyPreStreamError` with `finishReason: 'error'` rather than `'cancelled'`. See
|
|
740
|
+
* `cancelTurn()`'s Critical Invariant below for the full contract and why this residual gap is
|
|
741
|
+
* accepted for now.
|
|
742
|
+
*/
|
|
743
|
+
async maybeAutoCompact(abortSignal) {
|
|
744
|
+
if (this.harness.hasNativeAutoCompaction || abortSignal.aborted)
|
|
745
|
+
return;
|
|
746
|
+
const threshold = this.getAutoCompactionThreshold();
|
|
747
|
+
let occupancy;
|
|
748
|
+
try {
|
|
749
|
+
occupancy = await this.currentOccupancy();
|
|
750
|
+
}
|
|
751
|
+
catch (err) {
|
|
752
|
+
this.logBus.warn('auto-compaction occupancy read failed; skipping compaction for this turn', { agentId: this.agentId, threadId: this.threadId }, err instanceof Error ? err : undefined);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (occupancy === undefined || occupancy < threshold || abortSignal.aborted)
|
|
756
|
+
return;
|
|
757
|
+
try {
|
|
758
|
+
const newThreadId = await this.harness.compactThread(this.agentId, this.threadId);
|
|
759
|
+
const oldThreadId = this.threadId;
|
|
760
|
+
const contextWindow = this.getContextWindow();
|
|
761
|
+
this.threadId = newThreadId;
|
|
762
|
+
this.resetUsageState();
|
|
763
|
+
this.onThreadIdRotated?.(oldThreadId, newThreadId);
|
|
764
|
+
this.logBus.emitLog({
|
|
765
|
+
level: 'info',
|
|
766
|
+
message: 'Auto-compaction fired',
|
|
767
|
+
context: {
|
|
768
|
+
event_type: 'auto-compaction',
|
|
769
|
+
trigger: 'auto',
|
|
770
|
+
threshold,
|
|
771
|
+
agentId: this.agentId,
|
|
772
|
+
previousThreadId: oldThreadId,
|
|
773
|
+
newThreadId,
|
|
774
|
+
contextWindow,
|
|
775
|
+
occupancy,
|
|
776
|
+
outcome: 'compacted',
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
if (err instanceof AgentSDKError && err.type === AgentSDKErrorType.COMPACTION_FAILED) {
|
|
782
|
+
this.logBus.emitLog({
|
|
783
|
+
level: 'warn',
|
|
784
|
+
message: 'Auto-compaction failed',
|
|
785
|
+
context: {
|
|
786
|
+
event_type: 'auto-compaction',
|
|
787
|
+
trigger: 'auto',
|
|
788
|
+
threshold,
|
|
789
|
+
agentId: this.agentId,
|
|
790
|
+
previousThreadId: this.threadId,
|
|
791
|
+
contextWindow: this.getContextWindow(),
|
|
792
|
+
occupancy,
|
|
793
|
+
outcome: 'failed',
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
throw err;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* The occupancy ratio (0-1) `maybeAutoCompact` checks against the configured threshold,
|
|
803
|
+
* from whichever of two signals is available, in priority order:
|
|
804
|
+
*
|
|
805
|
+
* 1. **Live usage** — when {@link lastTurnUsageTrustworthy}, the same `effectiveInputTokens()
|
|
806
|
+
* / contextWindow` computation {@link getContextUsage} exposes as `usedFraction`.
|
|
807
|
+
* 2. **Transcript-size estimate** — otherwise (cold session, or the prior turn ended
|
|
808
|
+
* abnormally), a rough estimate from the persisted transcript's character count via
|
|
809
|
+
* {@link estimateTranscriptOccupancy}.
|
|
810
|
+
*
|
|
811
|
+
* Returns `undefined` when neither signal is available (a brand-new session with no
|
|
812
|
+
* completed turn and no persisted messages), so `maybeAutoCompact` never compacts a
|
|
813
|
+
* session with nothing to compact.
|
|
814
|
+
*/
|
|
815
|
+
async currentOccupancy() {
|
|
816
|
+
if (this.lastTurnUsageTrustworthy) {
|
|
817
|
+
const contextTokens = this.effectiveInputTokens();
|
|
818
|
+
if (contextTokens !== undefined) {
|
|
819
|
+
return clampFraction(contextTokens, this.getContextWindow());
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return this.estimateTranscriptOccupancy();
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Estimates occupancy from the thread's persisted transcript size (chars ÷
|
|
826
|
+
* {@link CHARS_PER_TOKEN} as a token-count proxy) when no trustworthy live usage reading is
|
|
827
|
+
* available. Returns `undefined` for an empty transcript so a brand-new thread never
|
|
828
|
+
* triggers compaction.
|
|
829
|
+
*/
|
|
830
|
+
async estimateTranscriptOccupancy() {
|
|
831
|
+
const messages = await this.harness.getMessages(this.agentId, this.threadId);
|
|
832
|
+
if (messages.length === 0)
|
|
833
|
+
return undefined;
|
|
834
|
+
const estimatedTokens = estimateTranscriptChars(messages) / CHARS_PER_TOKEN;
|
|
835
|
+
return clampFraction(estimatedTokens, this.getContextWindow());
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Re-points this session's inbound router-slice forwarding at a new slice, unsubscribing
|
|
839
|
+
* the old one first. Called by `DefaultAgent` after an auto-compaction rotation re-registers
|
|
840
|
+
* the telemetry-router slice under the new thread id, so any future thread-scoped
|
|
841
|
+
* harness-emitted event routes to this session under its new id rather than falling through
|
|
842
|
+
* to "unrouted". Not part of the public {@link ChatSession} interface — `DefaultAgent` holds
|
|
843
|
+
* the concrete `DefaultChatSession` type, same as {@link releaseWithoutEvent}.
|
|
844
|
+
*/
|
|
845
|
+
rebindInboundSlice(slice) {
|
|
846
|
+
for (const unsub of this.inboundUnsubs)
|
|
847
|
+
unsub();
|
|
848
|
+
this.inboundUnsubs.length = 0;
|
|
849
|
+
this.inboundUnsubs.push(slice.telemetry.forwardTo(this.telemetryBus), slice.log.forwardTo(this.logBus));
|
|
850
|
+
}
|
|
648
851
|
/**
|
|
649
852
|
* @requirements
|
|
650
853
|
* - IF `message` is a `string`, it MUST be formatted into a standard `Message` object array containing exactly one message.
|
|
@@ -936,23 +1139,26 @@ export class DefaultChatSession {
|
|
|
936
1139
|
* not throw.
|
|
937
1140
|
*
|
|
938
1141
|
* `startedAt` is the timestamp captured by {@link emitChatStreamStarted} so `durationMs`
|
|
939
|
-
* measures real elapsed time even for pre-stream rejections.
|
|
1142
|
+
* measures real elapsed time even for pre-stream rejections. `threadId` is the same
|
|
1143
|
+
* pre-auto-compaction-rotation id captured alongside `startedAt`, so this event stays paired
|
|
1144
|
+
* with its `chat-stream-started` even when the rejection surfaces after a mid-turn rotation
|
|
1145
|
+
* (e.g. `harness.stream()` itself fails on the post-rotation thread).
|
|
940
1146
|
*/
|
|
941
|
-
notifyPreStreamError(err, startedAt) {
|
|
1147
|
+
notifyPreStreamError(err, startedAt, threadId) {
|
|
942
1148
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
943
1149
|
this.chatEventBus.emit({ type: 'error', error });
|
|
944
1150
|
this.chatEventBus.emit({ type: 'finish', finishReason: 'error' });
|
|
945
1151
|
const finishedAt = this.clock.now();
|
|
946
1152
|
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
947
1153
|
// Tier-1 pair: share one tick across the telemetry event and its colocated log sibling.
|
|
948
|
-
this.telemetryBus.emitTelemetry({ type: 'chat-stream-error', agentId: this.agentId, threadId
|
|
1154
|
+
this.telemetryBus.emitTelemetry({ type: 'chat-stream-error', agentId: this.agentId, threadId, durationMs, error }, finishedAt);
|
|
949
1155
|
this.logBus.emitLog({
|
|
950
1156
|
level: 'error',
|
|
951
1157
|
message: 'Chat stream failed',
|
|
952
1158
|
context: {
|
|
953
1159
|
event_type: 'chat-stream-error',
|
|
954
1160
|
agentId: this.agentId,
|
|
955
|
-
threadId
|
|
1161
|
+
threadId,
|
|
956
1162
|
durationMs,
|
|
957
1163
|
},
|
|
958
1164
|
error,
|
|
@@ -72,6 +72,19 @@ export interface AgentHarness {
|
|
|
72
72
|
* per-agent accessors take the agent id as their first argument.
|
|
73
73
|
*/
|
|
74
74
|
readonly extensions: Record<string, unknown>;
|
|
75
|
+
/**
|
|
76
|
+
* `true` when this harness's underlying runtime already runs its own native
|
|
77
|
+
* auto-compaction mechanism, so the SDK's own turn-boundary auto-compaction
|
|
78
|
+
* trigger (`ChatSession`'s `maybeAutoCompact`) must NOT also run for this
|
|
79
|
+
* harness's sessions — running both would double-compact the same
|
|
80
|
+
* conversation via two uncoordinated mechanisms. Optional; a harness that
|
|
81
|
+
* omits the field (or sets it `false`) gets the SDK's trigger, which is the
|
|
82
|
+
* correct default for every harness with no native equivalent. A harness
|
|
83
|
+
* that sets `true` is expected to honor `AgentConfig.autoCompactionThreshold`
|
|
84
|
+
* itself, by forwarding it to its own native mechanism — the SDK never
|
|
85
|
+
* verifies this from the harness-agnostic side.
|
|
86
|
+
*/
|
|
87
|
+
readonly hasNativeAutoCompaction?: boolean;
|
|
75
88
|
/**
|
|
76
89
|
* Shut down the harness gracefully, releasing all resources.
|
|
77
90
|
* Disconnects MCP servers, closes storage connections, and
|
|
@@ -88,7 +88,28 @@ export type AgentConfig = {
|
|
|
88
88
|
* annotation-matcher rules don't fire on tools with no annotations.
|
|
89
89
|
*/
|
|
90
90
|
defaultToolDecision?: Decision;
|
|
91
|
+
/**
|
|
92
|
+
* Occupancy ratio (0-1) that triggers a compaction before the next turn's model call.
|
|
93
|
+
* Defaults to {@link DEFAULT_AUTO_COMPACTION_THRESHOLD} (0.9) when omitted. Values outside
|
|
94
|
+
* `[0.01, 0.99]` are clamped by {@link resolveAutoCompactionThreshold} — see its JSDoc for why.
|
|
95
|
+
*
|
|
96
|
+
* Auto-compaction is **always active** — there is no field to disable it. On a harness
|
|
97
|
+
* whose {@link AgentHarness.hasNativeAutoCompaction} is `true`, this value is forwarded to
|
|
98
|
+
* that harness's own native mechanism instead of running the SDK's turn-boundary trigger.
|
|
99
|
+
*/
|
|
100
|
+
autoCompactionThreshold?: number;
|
|
91
101
|
};
|
|
102
|
+
/** Default {@link AgentConfig.autoCompactionThreshold} occupancy ratio used when a threshold is not supplied. */
|
|
103
|
+
export declare const DEFAULT_AUTO_COMPACTION_THRESHOLD = 0.9;
|
|
104
|
+
/**
|
|
105
|
+
* Resolves an {@link AgentConfig.autoCompactionThreshold} value to its effective threshold:
|
|
106
|
+
* defaults to {@link DEFAULT_AUTO_COMPACTION_THRESHOLD} when `threshold` is omitted or not a
|
|
107
|
+
* finite number (`NaN`, `Infinity`, `-Infinity`), then clamps the result to
|
|
108
|
+
* `[0.01, 0.99]` — see the comment above {@link AUTO_COMPACTION_THRESHOLD_MIN} for why this
|
|
109
|
+
* clamps rather than throws (a live `chat()` turn is the wrong place to fail a config mistake
|
|
110
|
+
* that was already accepted at `createAgent`/`updateAgentConfig` time).
|
|
111
|
+
*/
|
|
112
|
+
export declare function resolveAutoCompactionThreshold(threshold?: number): number;
|
|
92
113
|
/**
|
|
93
114
|
* Resolves the per-agent OAuth {@link McpAuthProviders} map (remote MCP server
|
|
94
115
|
* name → provider) from the agent's id and the config the SDK has on file.
|
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
+
/** Default {@link AgentConfig.autoCompactionThreshold} occupancy ratio used when a threshold is not supplied. */
|
|
6
|
+
export const DEFAULT_AUTO_COMPACTION_THRESHOLD = 0.9;
|
|
7
|
+
/**
|
|
8
|
+
* Every harness must honor the same effective threshold for the same config value — that is the
|
|
9
|
+
* entire premise of a threshold-only (no on/off toggle) auto-compaction contract. An unvalidated
|
|
10
|
+
* out-of-range or non-numeric input would otherwise degrade differently per harness: on
|
|
11
|
+
* Mastra/OpenAI, a `threshold >= 1` makes the SDK's own `occupancy >= threshold` check
|
|
12
|
+
* (`occupancy` is itself clamped to `[0, 1]`) effectively unreachable — silently disabling
|
|
13
|
+
* auto-compaction on those two harnesses only, reproducing exactly the per-harness-meaningless
|
|
14
|
+
* "disabled" state this design exists to avoid. `[0.01, 0.99]` mirrors the Claude harness's own
|
|
15
|
+
* independent `toAutoCompactPctOverride` clamp, so every harness converges on the same effective
|
|
16
|
+
* value for any input, valid or not.
|
|
17
|
+
*/
|
|
18
|
+
const AUTO_COMPACTION_THRESHOLD_MIN = 0.01;
|
|
19
|
+
const AUTO_COMPACTION_THRESHOLD_MAX = 0.99;
|
|
20
|
+
/**
|
|
21
|
+
* Resolves an {@link AgentConfig.autoCompactionThreshold} value to its effective threshold:
|
|
22
|
+
* defaults to {@link DEFAULT_AUTO_COMPACTION_THRESHOLD} when `threshold` is omitted or not a
|
|
23
|
+
* finite number (`NaN`, `Infinity`, `-Infinity`), then clamps the result to
|
|
24
|
+
* `[0.01, 0.99]` — see the comment above {@link AUTO_COMPACTION_THRESHOLD_MIN} for why this
|
|
25
|
+
* clamps rather than throws (a live `chat()` turn is the wrong place to fail a config mistake
|
|
26
|
+
* that was already accepted at `createAgent`/`updateAgentConfig` time).
|
|
27
|
+
*/
|
|
28
|
+
export function resolveAutoCompactionThreshold(threshold) {
|
|
29
|
+
const resolved = threshold === undefined || !Number.isFinite(threshold) ? DEFAULT_AUTO_COMPACTION_THRESHOLD : threshold;
|
|
30
|
+
return Math.min(AUTO_COMPACTION_THRESHOLD_MAX, Math.max(AUTO_COMPACTION_THRESHOLD_MIN, resolved));
|
|
31
|
+
}
|
|
5
32
|
/**
|
|
6
33
|
* Converts a consumer-facing {@link AgentConfig} into a {@link HarnessAgentConfig}
|
|
7
34
|
* ready for the harness layer.
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { ResolverResult, ResolverTiers, ToolInvocation } from './policy-res
|
|
|
7
7
|
export type { ContextUsage, FinishReason, UsageMetadata } from './types/usage.js';
|
|
8
8
|
export type { AgentHooks, HooksForAgent, ToolResultRedactor, ToolResultRedactionInput, ToolResultRedactionResult, } from './types/redaction.js';
|
|
9
9
|
export type { AgentConfig, AgentStateUpdate, ConsumerMetadataMutation, HarnessAgentConfig, McpAuthProviderResolver, StreamOptions, } from './harness/harness-config.js';
|
|
10
|
-
export { DEFAULT_MAX_STEPS } from './harness/harness-config.js';
|
|
10
|
+
export { DEFAULT_MAX_STEPS, DEFAULT_AUTO_COMPACTION_THRESHOLD, resolveAutoCompactionThreshold, } from './harness/harness-config.js';
|
|
11
11
|
export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemoteServerConfig, McpOAuthClientProvider, McpAuthProviders, McpServerInfo, McpServerErrorCategory, McpServerErrorDetail, McpToolInfo, McpToolAnnotations, } from './mcp-config.js';
|
|
12
12
|
export { McpServerStatus, mcpServerConfigEqual } from './mcp-config.js';
|
|
13
13
|
export { Model, ModelName, createClaudeModel, Models, validateMultimodalFiles, ACCEPTED_MODEL_WIRE_IDS, } from './models/index.js';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `resolveToolApprovalPolicy` into their gate sites (Phase 2); consumers author
|
|
9
9
|
// `AgentConfig.toolPolicies` (directly or via `definePolicy`).
|
|
10
10
|
export { BUILT_IN_TOOL_POLICIES, SKILL_BRIDGE_SERVER_ID, definePolicy, matcherMatches, resolveToolApprovalPolicy, } from './policy-resolver.js';
|
|
11
|
-
export { DEFAULT_MAX_STEPS } from './harness/harness-config.js';
|
|
11
|
+
export { DEFAULT_MAX_STEPS, DEFAULT_AUTO_COMPACTION_THRESHOLD, resolveAutoCompactionThreshold, } from './harness/harness-config.js';
|
|
12
12
|
export { McpServerStatus, mcpServerConfigEqual } from './mcp-config.js';
|
|
13
13
|
export { Model, ModelName, createClaudeModel, Models, validateMultimodalFiles, ACCEPTED_MODEL_WIRE_IDS, } from './models/index.js';
|
|
14
14
|
export { MimeType } from './models/index.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Message } from '../types/messages.js';
|
|
2
|
+
/**
|
|
3
|
+
* Approximate characters-per-token ratio used to estimate context occupancy from raw transcript
|
|
4
|
+
* text when no live usage reading is available (cold session, or the last turn ended abnormally).
|
|
5
|
+
* A rough estimate is only ever used to decide whether to compact — the post-compaction turn's
|
|
6
|
+
* real usage reading is the source of truth.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CHARS_PER_TOKEN = 4;
|
|
9
|
+
/**
|
|
10
|
+
* Estimates the total character count of a message transcript, for use as a rough
|
|
11
|
+
* proxy for token count (via {@link CHARS_PER_TOKEN}) when no live usage reading is
|
|
12
|
+
* available. Sums plain string content directly; for structured `MessagePart[]`
|
|
13
|
+
* content, sums the text of `text` / `reasoning` parts, the serialized size of
|
|
14
|
+
* `tool-call` args and `tool-result` payloads, and the base64 payload length of
|
|
15
|
+
* `image` / `file` parts.
|
|
16
|
+
*/
|
|
17
|
+
export declare function estimateTranscriptChars(messages: Message[]): number;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Approximate characters-per-token ratio used to estimate context occupancy from raw transcript
|
|
7
|
+
* text when no live usage reading is available (cold session, or the last turn ended abnormally).
|
|
8
|
+
* A rough estimate is only ever used to decide whether to compact — the post-compaction turn's
|
|
9
|
+
* real usage reading is the source of truth.
|
|
10
|
+
*/
|
|
11
|
+
export const CHARS_PER_TOKEN = 4;
|
|
12
|
+
/**
|
|
13
|
+
* Estimates the total character count of a message transcript, for use as a rough
|
|
14
|
+
* proxy for token count (via {@link CHARS_PER_TOKEN}) when no live usage reading is
|
|
15
|
+
* available. Sums plain string content directly; for structured `MessagePart[]`
|
|
16
|
+
* content, sums the text of `text` / `reasoning` parts, the serialized size of
|
|
17
|
+
* `tool-call` args and `tool-result` payloads, and the base64 payload length of
|
|
18
|
+
* `image` / `file` parts.
|
|
19
|
+
*/
|
|
20
|
+
export function estimateTranscriptChars(messages) {
|
|
21
|
+
let total = 0;
|
|
22
|
+
for (const message of messages) {
|
|
23
|
+
if (typeof message.content === 'string') {
|
|
24
|
+
total += message.content.length;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const part of message.content) {
|
|
28
|
+
total += estimatePartChars(part);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return total;
|
|
33
|
+
}
|
|
34
|
+
function estimatePartChars(part) {
|
|
35
|
+
switch (part.type) {
|
|
36
|
+
case 'text':
|
|
37
|
+
case 'reasoning':
|
|
38
|
+
return part.text.length;
|
|
39
|
+
case 'tool-call':
|
|
40
|
+
return JSON.stringify(part.args).length;
|
|
41
|
+
case 'tool-result':
|
|
42
|
+
return JSON.stringify(part.result).length;
|
|
43
|
+
case 'image':
|
|
44
|
+
case 'file':
|
|
45
|
+
return part.data.length;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=transcript-occupancy.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -48,24 +48,24 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "^10.0.1",
|
|
51
|
-
"@salesforce/sfdx-agent-harness-claude": "0.
|
|
52
|
-
"@salesforce/sfdx-agent-harness-mastra": "0.
|
|
53
|
-
"@salesforce/sfdx-agent-harness-openai": "0.
|
|
51
|
+
"@salesforce/sfdx-agent-harness-claude": "0.78.0",
|
|
52
|
+
"@salesforce/sfdx-agent-harness-mastra": "0.81.0",
|
|
53
|
+
"@salesforce/sfdx-agent-harness-openai": "0.47.0",
|
|
54
54
|
"@types/node": "^22.20.1",
|
|
55
55
|
"@vitest/coverage-istanbul": "^4.1.11",
|
|
56
56
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
57
|
-
"eslint": "^10.
|
|
57
|
+
"eslint": "^10.10.0",
|
|
58
58
|
"eslint-config-prettier": "^10.1.8",
|
|
59
59
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
60
60
|
"eslint-plugin-import": "^2.32.0",
|
|
61
61
|
"eslint-plugin-n": "^18.3.0",
|
|
62
62
|
"globals": "^17.12.0",
|
|
63
|
-
"lint-staged": "^17.
|
|
63
|
+
"lint-staged": "^17.5.0",
|
|
64
64
|
"prettier": "^3.9.6",
|
|
65
65
|
"rimraf": "^6.1.3",
|
|
66
66
|
"tsx": "^4.23.13",
|
|
67
67
|
"typescript": "^7.0.2",
|
|
68
|
-
"typescript-eslint": "^8.
|
|
68
|
+
"typescript-eslint": "^8.70.0",
|
|
69
69
|
"vitest": "^4.1.11"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|