@salesforce/sfdx-agent-sdk 0.15.0 → 0.16.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/README.md +167 -87
- package/dist/chat-session.d.ts +43 -26
- package/dist/chat-session.js +34 -23
- package/dist/harness/agent-harness.d.ts +23 -16
- package/dist/harness/gen-sink.d.ts +41 -0
- package/dist/harness/gen-sink.js +88 -0
- package/dist/harness/index.d.ts +1 -0
- package/dist/harness/index.js +1 -0
- package/dist/harness/public.d.ts +49 -0
- package/dist/harness/public.js +10 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -47,7 +47,8 @@ for await (const event of eventStream) {
|
|
|
47
47
|
process.stdout.write(event.text);
|
|
48
48
|
} else if (event.type === 'error') {
|
|
49
49
|
console.error(event.error.message);
|
|
50
|
-
break
|
|
50
|
+
// Don't break — the SDK invariant guarantees a synthetic
|
|
51
|
+
// `FinishEvent('error')` follows; the loop exits naturally next tick.
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -152,31 +153,33 @@ keeps unparameterized call sites working.
|
|
|
152
153
|
|
|
153
154
|
A single conversation thread.
|
|
154
155
|
|
|
155
|
-
| Method | Signature
|
|
156
|
-
| ------------------- |
|
|
157
|
-
| `getId` | `() => string`
|
|
158
|
-
| `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>`
|
|
159
|
-
| `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<
|
|
160
|
-
| `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<
|
|
161
|
-
| `declineToolCall` | `(toolCallId: string) => Promise<
|
|
162
|
-
| `getMessageHistory` | `() => Promise<Message[]>`
|
|
163
|
-
| `clearHistory` | `() => Promise<void>`
|
|
164
|
-
| `getContextUsage` | `() => ContextUsage`
|
|
165
|
-
| `addContext` | `(message: string \| Message[]) => Promise<void>`
|
|
166
|
-
| `subscribe` | `(callback: (event: ChatEvent) => void) => void`
|
|
167
|
-
| `unsubscribe` | `(callback: (event: ChatEvent) => void) => void`
|
|
168
|
-
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe`
|
|
169
|
-
| `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe`
|
|
170
|
-
| `dispose` | `() => void`
|
|
156
|
+
| Method | Signature | Description |
|
|
157
|
+
| ------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `getId` | `() => string` | Session/thread identifier. |
|
|
159
|
+
| `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. |
|
|
160
|
+
| `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. |
|
|
161
|
+
| `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Approve a pending tool call. Control message on the existing turn — post-resume events flow on the same stream. |
|
|
162
|
+
| `declineToolCall` | `(toolCallId: string) => Promise<void>` | Decline a pending tool call. Control message on the existing turn — post-resume events flow on the same stream. |
|
|
163
|
+
| `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
|
|
164
|
+
| `clearHistory` | `() => Promise<void>` | Delete all messages. |
|
|
165
|
+
| `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
|
|
166
|
+
| `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response. |
|
|
167
|
+
| `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
|
|
168
|
+
| `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
|
|
169
|
+
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
|
|
170
|
+
| `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
|
|
171
|
+
| `dispose` | `() => void` | Release session-level event resources. Idempotent. |
|
|
171
172
|
|
|
172
173
|
### `ChatStreamResult`
|
|
173
174
|
|
|
174
|
-
Returned by `chat()
|
|
175
|
+
Returned by `chat()`. The single `eventStream` covers the entire chat turn — including post-resume events from
|
|
176
|
+
`submitToolResult` / `approveToolCall` / `declineToolCall`. Settle methods return `Promise<void>`; the consumer keeps
|
|
177
|
+
iterating the same `eventStream` until it sees a terminal `finish` event.
|
|
175
178
|
|
|
176
|
-
| Property | Type | Description
|
|
177
|
-
| ------------- | --------------------------- |
|
|
178
|
-
| `eventStream` | `AsyncGenerator<ChatEvent>` | Full lifecycle event stream.
|
|
179
|
-
| `textStream` | `AsyncGenerator<string>` | Convenience stream of text-only tokens
|
|
179
|
+
| Property | Type | Description |
|
|
180
|
+
| ------------- | --------------------------- | ---------------------------------------------------------------------------- |
|
|
181
|
+
| `eventStream` | `AsyncGenerator<ChatEvent>` | Full lifecycle event stream for the turn (one stream per `chat()` call). |
|
|
182
|
+
| `textStream` | `AsyncGenerator<string>` | Convenience stream of text-only tokens, derived from the same `eventStream`. |
|
|
180
183
|
|
|
181
184
|
### `ChatEvent`
|
|
182
185
|
|
|
@@ -560,17 +563,17 @@ try {
|
|
|
560
563
|
|
|
561
564
|
#### Streaming method failures
|
|
562
565
|
|
|
563
|
-
|
|
564
|
-
|
|
566
|
+
`chat()` opens a turn; `submitToolResult()` / `approveToolCall()` / `declineToolCall()` are control messages on the
|
|
567
|
+
existing turn. All four share a single failure contract. Subscribers registered via `subscribe()` are guaranteed to
|
|
565
568
|
receive a terminal event for every turn:
|
|
566
569
|
|
|
567
|
-
- **Pre-stream failure** (the harness rejects before producing a stream
|
|
568
|
-
by a `FinishEvent(finishReason: 'error')`, and the returned
|
|
569
|
-
call in `try/catch` if you need to handle this in the caller.
|
|
570
|
-
- **In-stream failure** (the
|
|
571
|
-
yields the `ErrorEvent` (synthesizing one from a thrown exception if needed) and, if no `FinishEvent` was
|
|
572
|
-
emitted, appends a synthetic `FinishEvent(finishReason: 'error')` at the end. The
|
|
573
|
-
iterate the stream to observe the failure.
|
|
570
|
+
- **Pre-stream failure** (the harness rejects before producing a stream, or a settle call rejects before it routes the
|
|
571
|
+
decision): subscribers receive an `ErrorEvent` followed by a `FinishEvent(finishReason: 'error')`, and the returned
|
|
572
|
+
promise then rejects with the original error. Wrap the call in `try/catch` if you need to handle this in the caller.
|
|
573
|
+
- **In-stream failure** (the active `eventStream` yields an `error` event or the underlying generator throws): the
|
|
574
|
+
stream yields the `ErrorEvent` (synthesizing one from a thrown exception if needed) and, if no `FinishEvent` was
|
|
575
|
+
already emitted, appends a synthetic `FinishEvent(finishReason: 'error')` at the end. The originating `chat()` promise
|
|
576
|
+
resolves normally; iterate the stream to observe the failure.
|
|
574
577
|
- **Calling a disposed session** throws `AgentSDKError('DISPOSED')` synchronously without notifying subscribers.
|
|
575
578
|
|
|
576
579
|
### MCP Server Configuration
|
|
@@ -613,99 +616,158 @@ reparsing namespaced tool names. `annotations` is `undefined` when the source di
|
|
|
613
616
|
`requireToolApproval` accepts a `boolean` (`true` is shorthand for `'serial'`) or one of the mode strings exposed via
|
|
614
617
|
the `ToolApprovalMode` type alias (`'serial' | 'batch'`):
|
|
615
618
|
|
|
616
|
-
- **`'serial'`** (the safe default) —
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
approvals before settling.
|
|
619
|
+
- **`'serial'`** (the safe default) — `tool-approval-request` events surface one at a time. The next request (if any)
|
|
620
|
+
arrives later on the same `eventStream` after the consumer settles the current one.
|
|
621
|
+
- **`'batch'`** — when the model emits parallel `tool_use` blocks, all approval-requests for that batch surface on the
|
|
622
|
+
same `eventStream` together, so the consumer can render a batch-approval card. The consumer must gather all approvals
|
|
623
|
+
before settling — a `break`-on-first-approval loop will hang the turn because Mastra/Claude won't continue emitting
|
|
624
|
+
tool-results until the consumer has settled the entire batch.
|
|
623
625
|
|
|
624
626
|
The SDK also exports `resolveToolApprovalMode(value)` — the canonical
|
|
625
627
|
`boolean | ToolApprovalMode | undefined → ToolApprovalMode | undefined` normalizer harness implementations should use to
|
|
626
628
|
dispatch (`undefined` / `false` → `undefined`, `true` → `'serial'`, strings pass through). Reject unknown strings via
|
|
627
629
|
`throw` instead of reimplementing the boolean-vs-string branch — the helper's defensive throw catches `as any` consumers
|
|
628
|
-
passing invalid values that would otherwise silently degrade to "approval gating on but no
|
|
630
|
+
passing invalid values that would otherwise silently degrade to "approval gating on but no coordinator allocated."
|
|
631
|
+
|
|
632
|
+
#### Single stream per turn
|
|
629
633
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
634
|
+
Issue [#529](https://github.com/forcedotcom/agentic-dx/issues/529) settled the harness contract: **one `eventStream` per
|
|
635
|
+
chat turn.** Settle calls (`approveToolCall` / `declineToolCall` / `submitToolResult`) return `Promise<void>` and behave
|
|
636
|
+
as control messages on the existing turn; the post-resume events (`tool-result`, follow-up `text-delta`, `step-finish`,
|
|
637
|
+
terminal `finish`) flow through the **same** `eventStream` the `chat()` call returned. The consumer should iterate that
|
|
638
|
+
one stream until it sees a terminal `finish` event, calling `approveToolCall` / `declineToolCall` / `submitToolResult`
|
|
639
|
+
in-line as approval-requests / consumer-tool-calls arrive.
|
|
633
640
|
|
|
634
|
-
#### Pattern
|
|
641
|
+
#### Pattern: settle in-line, continue iterating
|
|
642
|
+
|
|
643
|
+
The same shape works for `'serial'` and `'batch'` mode:
|
|
635
644
|
|
|
636
645
|
```typescript
|
|
637
646
|
const { eventStream } = await session.chat('Run the deployment', {
|
|
638
|
-
requireToolApproval: true, // or 'serial'
|
|
647
|
+
requireToolApproval: true, // or 'serial' / 'batch'
|
|
639
648
|
});
|
|
640
649
|
|
|
641
650
|
for await (const event of eventStream) {
|
|
642
651
|
if (event.type === 'tool-approval-request') {
|
|
643
652
|
const approved = await promptUser(event);
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
// process continuation
|
|
653
|
+
if (approved) {
|
|
654
|
+
await session.approveToolCall(event.toolCall.toolCallId);
|
|
655
|
+
} else {
|
|
656
|
+
await session.declineToolCall(event.toolCall.toolCallId);
|
|
649
657
|
}
|
|
650
|
-
break
|
|
658
|
+
// Do NOT break — keep iterating; tool-result and follow-up text arrive on the same stream.
|
|
659
|
+
} else if (event.type === 'tool-call' && event.serverName === undefined && isConsumerTool(event.toolName)) {
|
|
660
|
+
// Consumer-executed tool: run locally, return the result via submitToolResult.
|
|
661
|
+
const result = await runLocally(event.args);
|
|
662
|
+
await session.submitToolResult({
|
|
663
|
+
toolCallId: event.toolCallId,
|
|
664
|
+
toolName: event.toolName,
|
|
665
|
+
result,
|
|
666
|
+
});
|
|
667
|
+
} else if (event.type === 'text-delta') {
|
|
668
|
+
process.stdout.write(event.text);
|
|
669
|
+
} else if (event.type === 'finish') {
|
|
670
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
671
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
672
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
673
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
674
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
675
|
+
// close/return path.
|
|
676
|
+
break;
|
|
651
677
|
}
|
|
652
678
|
}
|
|
653
679
|
```
|
|
654
680
|
|
|
655
|
-
#### Pattern
|
|
681
|
+
#### Pattern variant: batch-collect approvals before settling
|
|
682
|
+
|
|
683
|
+
Same single-stream loop, just gather the batch before deciding (useful for "approve these N tools?" UI cards under
|
|
684
|
+
`requireToolApproval: 'batch'`):
|
|
656
685
|
|
|
657
686
|
```typescript
|
|
658
687
|
const { eventStream } = await session.chat('Run the deployment', {
|
|
659
688
|
requireToolApproval: 'batch',
|
|
660
689
|
});
|
|
661
690
|
|
|
662
|
-
const
|
|
691
|
+
const pendingBatch: ToolApprovalRequestEvent[] = [];
|
|
692
|
+
|
|
663
693
|
for await (const event of eventStream) {
|
|
664
694
|
if (event.type === 'tool-approval-request') {
|
|
665
|
-
|
|
695
|
+
pendingBatch.push(event);
|
|
696
|
+
// Heuristic: settle the batch on a quiet window OR after the expected count.
|
|
697
|
+
// Don't break — the harness needs us to settle so it can continue.
|
|
698
|
+
continue;
|
|
666
699
|
}
|
|
667
|
-
if (event.type === '
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const decisions = await promptUserForBatch(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
700
|
+
if (event.type === 'tool-call') {
|
|
701
|
+
// The harness emits `tool-approval-request` BEFORE `tool-call` in batch mode.
|
|
702
|
+
// Once we see a tool-call, the batch for this assistant message is complete.
|
|
703
|
+
if (pendingBatch.length > 0) {
|
|
704
|
+
const decisions = await promptUserForBatch(pendingBatch);
|
|
705
|
+
await Promise.all(
|
|
706
|
+
pendingBatch.map((req) =>
|
|
707
|
+
decisions.get(req.toolCall.toolCallId)
|
|
708
|
+
? session.approveToolCall(req.toolCall.toolCallId)
|
|
709
|
+
: session.declineToolCall(req.toolCall.toolCallId),
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
pendingBatch.length = 0;
|
|
713
|
+
}
|
|
714
|
+
} else if (event.type === 'text-delta') {
|
|
715
|
+
process.stdout.write(event.text);
|
|
716
|
+
} else if (event.type === 'finish') {
|
|
717
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
718
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
719
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
720
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
721
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
722
|
+
// close/return path.
|
|
723
|
+
break;
|
|
681
724
|
}
|
|
682
725
|
}
|
|
683
726
|
```
|
|
684
727
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
`
|
|
688
|
-
|
|
689
|
-
|
|
728
|
+
> **AsyncIterator gotcha:** the `for await` loop is the supported way to drain `eventStream`. If you instead drive the
|
|
729
|
+
> iterator manually (`iter.next()`), call **at most one** `next()` at a time and `await` the result before the next
|
|
730
|
+
> call. Calling `iter.next()` twice without awaiting the first orphans the first request — JS settles `.next()` calls
|
|
731
|
+
> FIFO, so the next yielded value satisfies the orphaned request and is invisible to the caller. Stick to `for await`
|
|
732
|
+
> unless you have a specific reason not to.
|
|
690
733
|
|
|
691
734
|
### Consumer-Executed Tools
|
|
692
735
|
|
|
736
|
+
Tools declared via `AgentConfig.tools` (no `execute` function) run on the consumer side: the model emits a `tool-call`
|
|
737
|
+
event, the consumer runs the tool locally, then calls `session.submitToolResult(...)` to feed the result back.
|
|
738
|
+
`submitToolResult` returns `Promise<void>`; the model's follow-up turn (and any subsequent `tool-call`s) flows through
|
|
739
|
+
the same `eventStream`:
|
|
740
|
+
|
|
693
741
|
```typescript
|
|
742
|
+
const { eventStream } = await session.chat('Look up the customer record for tenant T-42.');
|
|
743
|
+
|
|
694
744
|
for await (const event of eventStream) {
|
|
695
|
-
if (event.type === 'tool-call' && event.toolName === '
|
|
745
|
+
if (event.type === 'tool-call' && event.toolName === 'lookup_customer') {
|
|
696
746
|
const result = await runLocally(event.args);
|
|
697
|
-
|
|
747
|
+
await session.submitToolResult({
|
|
698
748
|
toolCallId: event.toolCallId,
|
|
699
749
|
toolName: event.toolName,
|
|
700
750
|
result,
|
|
701
751
|
});
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
752
|
+
// Do NOT break — the model's follow-up turn (text, more tool-calls, finish) arrives on the same stream.
|
|
753
|
+
} else if (event.type === 'text-delta') {
|
|
754
|
+
process.stdout.write(event.text);
|
|
755
|
+
} else if (event.type === 'finish') {
|
|
756
|
+
// Don't break on `'error'` directly — the SDK invariant guarantees an
|
|
757
|
+
// `error` event is followed by a synthetic `FinishEvent('error')` so
|
|
758
|
+
// the loop exits naturally on the next iteration. Breaking on `error`
|
|
759
|
+
// would finalize the iterator before the synthetic `finish` lands and
|
|
760
|
+
// bypass any per-turn cleanup the harness wires onto the source's
|
|
761
|
+
// close/return path.
|
|
762
|
+
break;
|
|
705
763
|
}
|
|
706
764
|
}
|
|
707
765
|
```
|
|
708
766
|
|
|
767
|
+
When `requireToolApproval: true` is also set, consumer-executed tools bypass the approval gate by construction (per
|
|
768
|
+
`StreamOptions.requireToolApproval` JSDoc). They surface as a normal `tool-call` event without a preceding
|
|
769
|
+
`tool-approval-request`. Built-in / MCP tools still gate normally.
|
|
770
|
+
|
|
709
771
|
### Connectivity Resolution
|
|
710
772
|
|
|
711
773
|
#### `ResolvedConnectivity`
|
|
@@ -843,15 +905,33 @@ The SDK ships no harness implementation. Consumers pick one by passing a `Harnes
|
|
|
843
905
|
[`@salesforce/sfdx-agent-harness-mastra`](../sfdx-agent-harness-mastra); additional harnesses can ship as independent
|
|
844
906
|
npm packages that depend on this SDK as a `peerDependency`.
|
|
845
907
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
908
|
+
### Two import surfaces
|
|
909
|
+
|
|
910
|
+
This package publishes two ESM entry points:
|
|
911
|
+
|
|
912
|
+
- **`@salesforce/sfdx-agent-sdk`** — consumer surface. Everything an application calling `createAgentManager` needs:
|
|
913
|
+
`Agent`, `ChatSession`, `ChatEvent`, `MessagePart`, `AgentConfig`, `AgentSDKError`, etc. The harness contract
|
|
914
|
+
interfaces (`AgentHarness`, `HarnessFactory`, `WithAgentConfig`, `ConfigOf`) also appear here as **type-only**
|
|
915
|
+
references so consumer code can write `AgentManager<H extends AgentHarness>` without reaching into the
|
|
916
|
+
harness-implementation surface.
|
|
917
|
+
- **`@salesforce/sfdx-agent-sdk/harness`** — harness-implementation surface. Harness authors import the contract types
|
|
918
|
+
and the helpers from here. Consumer applications cannot reach these symbols from the bare specifier — the bundler / TS
|
|
919
|
+
resolver errors at the import site, which is the right outcome for any consumer who reaches for a primitive they don't
|
|
920
|
+
need.
|
|
921
|
+
|
|
922
|
+
> **Build-tool requirement:** the `/harness` subpath is resolved via the `package.json` `exports` field, which requires
|
|
923
|
+
> `moduleResolution: "node16" | "nodenext" | "bundler"` in `tsconfig.json`. Legacy `"node"` resolution silently won't
|
|
924
|
+
> see the subpath. Modern bundlers (Vite, esbuild, Webpack 5+, tsup, Rollup with `@rollup/plugin-node-resolve` v15+)
|
|
925
|
+
> resolve it natively. This is a harness-author concern only; consumer applications never touch the subpath.
|
|
926
|
+
|
|
927
|
+
| Export | Surface | Role |
|
|
928
|
+
| ----------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
929
|
+
| `HarnessFactory<H>` | Type only on bare; value+type on `/harness` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
|
|
930
|
+
| `AgentHarness` | Type only on bare; type on `/harness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
|
|
931
|
+
| `SUPPORTED_PROTOCOL_VERSIONS` | `/harness` only | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
|
|
932
|
+
| `HarnessBusOwner` | `/harness` only | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
|
|
933
|
+
| `lowerStreamInput` | `/harness` only | Validates a `MessagePart[]` and lowers each input part to your runtime's content-block shape. Use it in `stream()` so multimodal caps and `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics match every other harness. |
|
|
934
|
+
| `GenSink<T>` | `/harness` only | Buffered async-generator wrapper for routing `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`. Single-iteration: calling `generator()` twice throws — sinks have one waiter slot and one buffer, two iterators race on both. |
|
|
855
935
|
|
|
856
936
|
Minimal skeleton:
|
|
857
937
|
|
|
@@ -861,7 +941,7 @@ import {
|
|
|
861
941
|
type HarnessFactory,
|
|
862
942
|
HarnessBusOwner,
|
|
863
943
|
SUPPORTED_PROTOCOL_VERSIONS,
|
|
864
|
-
} from '@salesforce/sfdx-agent-sdk';
|
|
944
|
+
} from '@salesforce/sfdx-agent-sdk/harness';
|
|
865
945
|
|
|
866
946
|
class MyHarness implements AgentHarness {
|
|
867
947
|
readonly harnessId = 'my-harness';
|
package/dist/chat-session.d.ts
CHANGED
|
@@ -62,11 +62,17 @@ export interface ChatSession {
|
|
|
62
62
|
*
|
|
63
63
|
* "Client-side tool" means a tool you declared in {@link AgentConfig.tools}
|
|
64
64
|
* without an `execute` function — the SDK registers its name + schema with the
|
|
65
|
-
* model but does not run it. When the model calls one, the chat eventStream
|
|
66
|
-
* emits a `tool-call` event
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
65
|
+
* model but does not run it. When the model calls one, the chat `eventStream`
|
|
66
|
+
* emits a `tool-call` event. Your application runs the tool however it likes
|
|
67
|
+
* (HTTP call, DB query, UI prompt, etc.) and calls this method with the result;
|
|
68
|
+
* the agent loop resumes and the post-resume events (`tool-result`, model
|
|
69
|
+
* follow-up text, terminal `finish`) arrive on the **same** `eventStream`
|
|
70
|
+
* the original {@link chat} call returned. The consumer keeps iterating it.
|
|
71
|
+
*
|
|
72
|
+
* Returns `Promise<void>` once the harness has accepted the result. The
|
|
73
|
+
* promise rejects on pre-stream failure (the `chat()`-returned subscribers
|
|
74
|
+
* still observe `ErrorEvent` + `FinishEvent` before the rejection so the
|
|
75
|
+
* subscribe-side contract holds).
|
|
70
76
|
*
|
|
71
77
|
* Use this method ONLY for client-side tools. Tools provided via
|
|
72
78
|
* {@link AgentConfig.mcpServers} are executed by the harness — their results
|
|
@@ -74,23 +80,22 @@ export interface ChatSession {
|
|
|
74
80
|
* Human-in-the-loop approval of harness-executed tools uses
|
|
75
81
|
* {@link approveToolCall} / {@link declineToolCall}, not this method.
|
|
76
82
|
*
|
|
77
|
-
* On pre-stream failure, subscribers are notified with `ErrorEvent` + `FinishEvent` before
|
|
78
|
-
* the returned promise rejects. See the interface-level "Failure handling" notes for details.
|
|
79
|
-
*
|
|
80
83
|
* @param toolResult - The completed tool execution result. `toolCallId` and
|
|
81
84
|
* `toolName` MUST match the values from the originating `tool-call` event.
|
|
82
85
|
*/
|
|
83
|
-
submitToolResult(toolResult: ToolResultInfo): Promise<
|
|
86
|
+
submitToolResult(toolResult: ToolResultInfo): Promise<void>;
|
|
84
87
|
/**
|
|
85
88
|
* Approve a pending tool call, allowing the harness to execute it.
|
|
86
89
|
* Called after receiving a `tool-approval-request` event from the stream.
|
|
87
90
|
*
|
|
88
|
-
* Returns
|
|
89
|
-
* executes the
|
|
90
|
-
*
|
|
91
|
+
* Returns `Promise<void>` once the harness has accepted the approval. The
|
|
92
|
+
* harness then executes the tool and emits the resulting events
|
|
93
|
+
* (`tool-result`, model follow-up text, terminal `finish`) on the **same**
|
|
94
|
+
* `eventStream` the original {@link chat} call returned. The consumer keeps
|
|
95
|
+
* iterating it.
|
|
91
96
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
97
|
+
* The promise rejects on pre-stream failure; subscribers still observe
|
|
98
|
+
* `ErrorEvent` + `FinishEvent` on the chat stream before the rejection.
|
|
94
99
|
*
|
|
95
100
|
* @param toolCallId - ID of the pending tool call to approve.
|
|
96
101
|
* @param options - Optional approval metadata.
|
|
@@ -101,21 +106,20 @@ export interface ChatSession {
|
|
|
101
106
|
*/
|
|
102
107
|
approveToolCall(toolCallId: string, options?: {
|
|
103
108
|
remember?: boolean;
|
|
104
|
-
}): Promise<
|
|
109
|
+
}): Promise<void>;
|
|
105
110
|
/**
|
|
106
111
|
* Decline a pending tool call. The stream resumes with the model
|
|
107
|
-
* acknowledging the decline and potentially suggesting alternatives
|
|
112
|
+
* acknowledging the decline and potentially suggesting alternatives —
|
|
113
|
+
* those events arrive on the **same** `eventStream` the original
|
|
114
|
+
* {@link chat} call returned.
|
|
108
115
|
*
|
|
109
|
-
* Returns
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* On pre-stream failure, subscribers are notified with `ErrorEvent` + `FinishEvent` before
|
|
114
|
-
* the returned promise rejects. See the interface-level "Failure handling" notes for details.
|
|
116
|
+
* Returns `Promise<void>` once the harness has accepted the decline. The
|
|
117
|
+
* promise rejects on pre-stream failure; subscribers still observe
|
|
118
|
+
* `ErrorEvent` + `FinishEvent` on the chat stream before the rejection.
|
|
115
119
|
*
|
|
116
120
|
* @param toolCallId - ID of the pending tool call to decline.
|
|
117
121
|
*/
|
|
118
|
-
declineToolCall(toolCallId: string): Promise<
|
|
122
|
+
declineToolCall(toolCallId: string): Promise<void>;
|
|
119
123
|
/**
|
|
120
124
|
* Retrieve message history for this session.
|
|
121
125
|
*
|
|
@@ -246,7 +250,7 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
246
250
|
* - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
|
|
247
251
|
* before returning a stream result.
|
|
248
252
|
*/
|
|
249
|
-
submitToolResult(toolResult: ToolResultInfo): Promise<
|
|
253
|
+
submitToolResult(toolResult: ToolResultInfo): Promise<void>;
|
|
250
254
|
/**
|
|
251
255
|
* @requirements
|
|
252
256
|
* - MUST yield each event from the provided `stream`.
|
|
@@ -290,7 +294,7 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
290
294
|
*/
|
|
291
295
|
approveToolCall(toolCallId: string, _options?: {
|
|
292
296
|
remember?: boolean;
|
|
293
|
-
}): Promise<
|
|
297
|
+
}): Promise<void>;
|
|
294
298
|
/**
|
|
295
299
|
* @requirements
|
|
296
300
|
* - MUST delegate to `this.harness.declineToolCall()`, passing `this.agentId`, `this.threadId`, and `toolCallId`.
|
|
@@ -302,7 +306,7 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
302
306
|
* - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
|
|
303
307
|
* before returning a stream result.
|
|
304
308
|
*/
|
|
305
|
-
declineToolCall(toolCallId: string): Promise<
|
|
309
|
+
declineToolCall(toolCallId: string): Promise<void>;
|
|
306
310
|
/**
|
|
307
311
|
* @requirements
|
|
308
312
|
* - MUST delegate to `this.harness.getMessages()`, passing `this.agentId` and `this.threadId`.
|
|
@@ -395,5 +399,18 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
395
399
|
* measures real elapsed time even for pre-stream rejections.
|
|
396
400
|
*/
|
|
397
401
|
private notifyPreStreamError;
|
|
402
|
+
/**
|
|
403
|
+
* issue #529 contract change: a settle call (`approveToolCall` /
|
|
404
|
+
* `declineToolCall` / `submitToolResult`) rejected. The settle's
|
|
405
|
+
* Promise is the consumer's primary failure surface, but subscribers
|
|
406
|
+
* registered via {@link ChatSession.subscribe} also expect to observe
|
|
407
|
+
* `error + finish` events so a UI bound to the chat stream can
|
|
408
|
+
* render the failure. Emit those without firing
|
|
409
|
+
* `chat-stream-error` telemetry — chat-stream-* telemetry is owned
|
|
410
|
+
* by the chat() lifecycle, not by settle calls (issue #529: one
|
|
411
|
+
* chat-stream-started/completed/error pair per turn, not per
|
|
412
|
+
* settle).
|
|
413
|
+
*/
|
|
414
|
+
private notifySettleRejection;
|
|
398
415
|
private assertNotDisposed;
|
|
399
416
|
}
|
package/dist/chat-session.js
CHANGED
|
@@ -109,16 +109,16 @@ export class DefaultChatSession {
|
|
|
109
109
|
*/
|
|
110
110
|
async submitToolResult(toolResult) {
|
|
111
111
|
this.assertNotDisposed();
|
|
112
|
-
|
|
112
|
+
// issue #529 contract change: settle calls are control messages on the
|
|
113
|
+
// existing chat() turn's stream — they don't open a new stream and
|
|
114
|
+
// they don't emit chat-stream-started/completed. The post-resume
|
|
115
|
+
// events flow through the harness's existing turn sink, which the
|
|
116
|
+
// consumer's chat()-returned eventStream is already iterating.
|
|
113
117
|
try {
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
textStream: result.textStream,
|
|
117
|
-
eventStream: this.wrapEventStream(result.eventStream, startedAt),
|
|
118
|
-
};
|
|
118
|
+
await this.harness.submitToolResult(this.agentId, this.threadId, toolResult);
|
|
119
119
|
}
|
|
120
120
|
catch (err) {
|
|
121
|
-
this.
|
|
121
|
+
this.notifySettleRejection(err);
|
|
122
122
|
throw err;
|
|
123
123
|
}
|
|
124
124
|
}
|
|
@@ -246,19 +246,17 @@ export class DefaultChatSession {
|
|
|
246
246
|
*/
|
|
247
247
|
async approveToolCall(toolCallId, _options) {
|
|
248
248
|
this.assertNotDisposed();
|
|
249
|
-
|
|
249
|
+
// issue #529 contract change: see `submitToolResult` for the rationale.
|
|
250
|
+
// Settle is a control message on the existing turn; events flow on
|
|
251
|
+
// the chat()-returned stream.
|
|
250
252
|
try {
|
|
251
|
-
|
|
252
|
-
this.emitToolApprovalResolved(toolCallId, true);
|
|
253
|
-
return {
|
|
254
|
-
textStream: result.textStream,
|
|
255
|
-
eventStream: this.wrapEventStream(result.eventStream, startedAt),
|
|
256
|
-
};
|
|
253
|
+
await this.harness.approveToolCall(this.agentId, this.threadId, toolCallId);
|
|
257
254
|
}
|
|
258
255
|
catch (err) {
|
|
259
|
-
this.
|
|
256
|
+
this.notifySettleRejection(err);
|
|
260
257
|
throw err;
|
|
261
258
|
}
|
|
259
|
+
this.emitToolApprovalResolved(toolCallId, true);
|
|
262
260
|
}
|
|
263
261
|
/**
|
|
264
262
|
* @requirements
|
|
@@ -273,19 +271,15 @@ export class DefaultChatSession {
|
|
|
273
271
|
*/
|
|
274
272
|
async declineToolCall(toolCallId) {
|
|
275
273
|
this.assertNotDisposed();
|
|
276
|
-
|
|
274
|
+
// issue #529 contract change: see `submitToolResult` for the rationale.
|
|
277
275
|
try {
|
|
278
|
-
|
|
279
|
-
this.emitToolApprovalResolved(toolCallId, false);
|
|
280
|
-
return {
|
|
281
|
-
textStream: result.textStream,
|
|
282
|
-
eventStream: this.wrapEventStream(result.eventStream, startedAt),
|
|
283
|
-
};
|
|
276
|
+
await this.harness.declineToolCall(this.agentId, this.threadId, toolCallId);
|
|
284
277
|
}
|
|
285
278
|
catch (err) {
|
|
286
|
-
this.
|
|
279
|
+
this.notifySettleRejection(err);
|
|
287
280
|
throw err;
|
|
288
281
|
}
|
|
282
|
+
this.emitToolApprovalResolved(toolCallId, false);
|
|
289
283
|
}
|
|
290
284
|
/**
|
|
291
285
|
* @requirements
|
|
@@ -527,6 +521,23 @@ export class DefaultChatSession {
|
|
|
527
521
|
error,
|
|
528
522
|
});
|
|
529
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* issue #529 contract change: a settle call (`approveToolCall` /
|
|
526
|
+
* `declineToolCall` / `submitToolResult`) rejected. The settle's
|
|
527
|
+
* Promise is the consumer's primary failure surface, but subscribers
|
|
528
|
+
* registered via {@link ChatSession.subscribe} also expect to observe
|
|
529
|
+
* `error + finish` events so a UI bound to the chat stream can
|
|
530
|
+
* render the failure. Emit those without firing
|
|
531
|
+
* `chat-stream-error` telemetry — chat-stream-* telemetry is owned
|
|
532
|
+
* by the chat() lifecycle, not by settle calls (issue #529: one
|
|
533
|
+
* chat-stream-started/completed/error pair per turn, not per
|
|
534
|
+
* settle).
|
|
535
|
+
*/
|
|
536
|
+
notifySettleRejection(err) {
|
|
537
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
538
|
+
this.chatEventBus.emit({ type: 'error', error });
|
|
539
|
+
this.chatEventBus.emit({ type: 'finish', finishReason: 'error' });
|
|
540
|
+
}
|
|
530
541
|
assertNotDisposed() {
|
|
531
542
|
if (this.disposed) {
|
|
532
543
|
throw new AgentSDKError('ChatSession has been disposed.', AgentSDKErrorType.DISPOSED);
|
|
@@ -238,49 +238,56 @@ export interface AgentHarness {
|
|
|
238
238
|
stream(agentId: string, threadId: string, message: string | MessagePart[], options?: StreamOptions): Promise<ChatStreamResult>;
|
|
239
239
|
/**
|
|
240
240
|
* Feed the result of a **consumer-executed (client-side) tool** back into the
|
|
241
|
-
* conversation
|
|
241
|
+
* conversation. Implements the consumer-facing
|
|
242
242
|
* {@link ChatSession.submitToolResult} contract: see that JSDoc for the
|
|
243
243
|
* full consumer-level semantics.
|
|
244
244
|
*
|
|
245
245
|
* Called only for tools declared in {@link HarnessAgentConfig.tools} (no
|
|
246
246
|
* `execute` function). Tools provided via `mcpServers` are executed by the
|
|
247
|
-
* harness directly and never reach this method.
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
247
|
+
* harness directly and never reach this method.
|
|
248
|
+
*
|
|
249
|
+
* Returns `Promise<void>` once the harness has accepted the tool result
|
|
250
|
+
* (the parked bridge handler resolved). Post-resume events
|
|
251
|
+
* (`tool-result`, model follow-up text, terminal `finish`) flow through
|
|
252
|
+
* the SAME `ChatStreamResult.eventStream` returned by the most recent
|
|
253
|
+
* {@link stream} call on the thread — the consumer is still iterating
|
|
254
|
+
* that stream and sees the events arrive.
|
|
251
255
|
*
|
|
252
256
|
* @param agentId - ID of the agent.
|
|
253
257
|
* @param threadId - ID of the conversation thread.
|
|
254
258
|
* @param toolResult - The completed tool execution result; `toolCallId`
|
|
255
259
|
* matches the id from the originating `tool-call` event.
|
|
256
260
|
*/
|
|
257
|
-
submitToolResult(agentId: string, threadId: string, toolResult: ToolResultInfo): Promise<
|
|
261
|
+
submitToolResult(agentId: string, threadId: string, toolResult: ToolResultInfo): Promise<void>;
|
|
258
262
|
/**
|
|
259
263
|
* Approve a pending tool call, allowing the harness to execute it.
|
|
260
264
|
* Called after receiving a `tool-approval-request` event.
|
|
261
265
|
*
|
|
262
|
-
* Returns
|
|
263
|
-
*
|
|
264
|
-
*
|
|
266
|
+
* Returns `Promise<void>` once the harness has accepted the approval
|
|
267
|
+
* (the parked PreToolUse hook resolved on Claude; the resume call was
|
|
268
|
+
* dispatched on Mastra). Post-resume events flow through the SAME
|
|
269
|
+
* `ChatStreamResult.eventStream` returned by the most recent
|
|
270
|
+
* {@link stream} call on the thread.
|
|
265
271
|
*
|
|
266
272
|
* @param agentId - ID of the agent.
|
|
267
273
|
* @param threadId - ID of the conversation thread (needed for harness-internal state lookup).
|
|
268
274
|
* @param toolCallId - ID of the tool call to approve.
|
|
269
275
|
*/
|
|
270
|
-
approveToolCall(agentId: string, threadId: string, toolCallId: string): Promise<
|
|
276
|
+
approveToolCall(agentId: string, threadId: string, toolCallId: string): Promise<void>;
|
|
271
277
|
/**
|
|
272
|
-
* Decline a pending tool call. The harness resumes the
|
|
273
|
-
* model acknowledging the decline and potentially suggesting
|
|
278
|
+
* Decline a pending tool call. The harness resumes the agentic loop with
|
|
279
|
+
* the model acknowledging the decline and potentially suggesting
|
|
280
|
+
* alternatives.
|
|
274
281
|
*
|
|
275
|
-
* Returns
|
|
276
|
-
*
|
|
277
|
-
*
|
|
282
|
+
* Returns `Promise<void>` once the harness has accepted the decline.
|
|
283
|
+
* Post-resume events flow through the SAME `ChatStreamResult.eventStream`
|
|
284
|
+
* returned by the most recent {@link stream} call on the thread.
|
|
278
285
|
*
|
|
279
286
|
* @param agentId - ID of the agent.
|
|
280
287
|
* @param threadId - ID of the conversation thread.
|
|
281
288
|
* @param toolCallId - ID of the tool call to decline.
|
|
282
289
|
*/
|
|
283
|
-
declineToolCall(agentId: string, threadId: string, toolCallId: string): Promise<
|
|
290
|
+
declineToolCall(agentId: string, threadId: string, toolCallId: string): Promise<void>;
|
|
284
291
|
/**
|
|
285
292
|
* Retrieve message history for a thread.
|
|
286
293
|
*
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffered async-generator wrapper used by `AgentHarness` implementations to
|
|
3
|
+
* deliver `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`.
|
|
4
|
+
*
|
|
5
|
+
* The buffer holds events pushed before the consumer starts iterating, so the
|
|
6
|
+
* harness can begin populating the sink eagerly without coordinating with the
|
|
7
|
+
* consumer's `for await` loop. Events pushed after iteration starts unblock
|
|
8
|
+
* the awaiting consumer via an internal Promise waiter.
|
|
9
|
+
*
|
|
10
|
+
* # Single-iteration invariant
|
|
11
|
+
*
|
|
12
|
+
* `generator()` may be called **at most once** per sink. Sinks are stateful
|
|
13
|
+
* (single waiter slot, single buffer); two iterators on the same sink race on
|
|
14
|
+
* both, so two iterators is always a bug. Calling `generator()` a second time
|
|
15
|
+
* throws — the alternative (silently allowing the second call to share the
|
|
16
|
+
* buffer / waiter with the first) produced the deadlock-class bug catalogued
|
|
17
|
+
* in [issue #529](https://github.com/forcedotcom/agentic-dx/issues/529)
|
|
18
|
+
* Finding 4. Surface failures at construction-time, not at deadlock-time.
|
|
19
|
+
*
|
|
20
|
+
* # `isEnded()`
|
|
21
|
+
*
|
|
22
|
+
* Exposes the closed state for routing logic that needs to skip pushes onto
|
|
23
|
+
* a sink that's already been retired. Routing for a closed sink should
|
|
24
|
+
* silently no-op with a debug log rather than throw — the late event is
|
|
25
|
+
* upstream noise, not a programmer error.
|
|
26
|
+
*
|
|
27
|
+
* Exported from the public `index.ts` so third-party `AgentHarness`
|
|
28
|
+
* implementations in sibling packages can reuse it.
|
|
29
|
+
*/
|
|
30
|
+
export declare class GenSink<T> {
|
|
31
|
+
private readonly buffered;
|
|
32
|
+
private ended;
|
|
33
|
+
private waiter;
|
|
34
|
+
private generated;
|
|
35
|
+
push(event: T): void;
|
|
36
|
+
end(): void;
|
|
37
|
+
isEnded(): boolean;
|
|
38
|
+
generator(): AsyncGenerator<T, void>;
|
|
39
|
+
private iterate;
|
|
40
|
+
private wake;
|
|
41
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Buffered async-generator wrapper used by `AgentHarness` implementations to
|
|
7
|
+
* deliver `ChatEvent`s to a consumer's `ChatStreamResult.eventStream`.
|
|
8
|
+
*
|
|
9
|
+
* The buffer holds events pushed before the consumer starts iterating, so the
|
|
10
|
+
* harness can begin populating the sink eagerly without coordinating with the
|
|
11
|
+
* consumer's `for await` loop. Events pushed after iteration starts unblock
|
|
12
|
+
* the awaiting consumer via an internal Promise waiter.
|
|
13
|
+
*
|
|
14
|
+
* # Single-iteration invariant
|
|
15
|
+
*
|
|
16
|
+
* `generator()` may be called **at most once** per sink. Sinks are stateful
|
|
17
|
+
* (single waiter slot, single buffer); two iterators on the same sink race on
|
|
18
|
+
* both, so two iterators is always a bug. Calling `generator()` a second time
|
|
19
|
+
* throws — the alternative (silently allowing the second call to share the
|
|
20
|
+
* buffer / waiter with the first) produced the deadlock-class bug catalogued
|
|
21
|
+
* in [issue #529](https://github.com/forcedotcom/agentic-dx/issues/529)
|
|
22
|
+
* Finding 4. Surface failures at construction-time, not at deadlock-time.
|
|
23
|
+
*
|
|
24
|
+
* # `isEnded()`
|
|
25
|
+
*
|
|
26
|
+
* Exposes the closed state for routing logic that needs to skip pushes onto
|
|
27
|
+
* a sink that's already been retired. Routing for a closed sink should
|
|
28
|
+
* silently no-op with a debug log rather than throw — the late event is
|
|
29
|
+
* upstream noise, not a programmer error.
|
|
30
|
+
*
|
|
31
|
+
* Exported from the public `index.ts` so third-party `AgentHarness`
|
|
32
|
+
* implementations in sibling packages can reuse it.
|
|
33
|
+
*/
|
|
34
|
+
export class GenSink {
|
|
35
|
+
buffered = [];
|
|
36
|
+
ended = false;
|
|
37
|
+
waiter = null;
|
|
38
|
+
generated = false;
|
|
39
|
+
push(event) {
|
|
40
|
+
if (this.ended)
|
|
41
|
+
return;
|
|
42
|
+
this.buffered.push(event);
|
|
43
|
+
this.wake();
|
|
44
|
+
}
|
|
45
|
+
end() {
|
|
46
|
+
if (this.ended)
|
|
47
|
+
return;
|
|
48
|
+
this.ended = true;
|
|
49
|
+
this.wake();
|
|
50
|
+
}
|
|
51
|
+
isEnded() {
|
|
52
|
+
return this.ended;
|
|
53
|
+
}
|
|
54
|
+
generator() {
|
|
55
|
+
// Synchronous guard. Must live outside the async generator body —
|
|
56
|
+
// async generators are lazy (the body does not execute until the
|
|
57
|
+
// first `next()`), so a `throw` inside the body would not surface
|
|
58
|
+
// at the second `generator()` call site as expected.
|
|
59
|
+
if (this.generated) {
|
|
60
|
+
throw new Error('GenSink.generator(): sinks are single-iteration. ' +
|
|
61
|
+
'Two iterators on one sink race on the buffer and waiter slot. ' +
|
|
62
|
+
'See https://github.com/forcedotcom/agentic-dx/issues/529 (Finding 4).');
|
|
63
|
+
}
|
|
64
|
+
this.generated = true;
|
|
65
|
+
return this.iterate();
|
|
66
|
+
}
|
|
67
|
+
async *iterate() {
|
|
68
|
+
while (true) {
|
|
69
|
+
if (this.buffered.length > 0) {
|
|
70
|
+
yield this.buffered.shift();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (this.ended)
|
|
74
|
+
return;
|
|
75
|
+
await new Promise((resolve) => {
|
|
76
|
+
this.waiter = { resolve };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
wake() {
|
|
81
|
+
const w = this.waiter;
|
|
82
|
+
if (w) {
|
|
83
|
+
this.waiter = null;
|
|
84
|
+
w.resolve();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=gen-sink.js.map
|
package/dist/harness/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export type { AgentHarness, WithAgentConfig, ConfigOf } from './agent-harness.js
|
|
|
2
2
|
export type { HarnessFactory } from './harness-factory.js';
|
|
3
3
|
export type { AgentConfig, StreamOptions } from './harness-config.js';
|
|
4
4
|
export { toHarnessConfig, DEFAULT_MAX_STEPS } from './harness-config.js';
|
|
5
|
+
export { GenSink } from './gen-sink.js';
|
package/dist/harness/index.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-implementation public surface.
|
|
3
|
+
*
|
|
4
|
+
* This module is reachable as `@salesforce/sfdx-agent-sdk/harness` —
|
|
5
|
+
* **never** import directly from `dist/` or via deep relative paths.
|
|
6
|
+
*
|
|
7
|
+
* # When to import from here
|
|
8
|
+
*
|
|
9
|
+
* Use this surface when implementing an `AgentHarness` (e.g.
|
|
10
|
+
* `@salesforce/sfdx-agent-harness-claude`,
|
|
11
|
+
* `@salesforce/sfdx-agent-harness-mastra`). It re-exports both the
|
|
12
|
+
* harness contract types (`AgentHarness`, `HarnessFactory`) and the
|
|
13
|
+
* harness-implementation helpers consumer applications must not touch
|
|
14
|
+
* (`HarnessBusOwner`, `GenSink`, `lowerStreamInput`,
|
|
15
|
+
* `SUPPORTED_PROTOCOL_VERSIONS`).
|
|
16
|
+
*
|
|
17
|
+
* # Why this is a separate subpath
|
|
18
|
+
*
|
|
19
|
+
* Consumer applications (anything that calls `createAgentManager` and
|
|
20
|
+
* iterates `ChatStreamResult`) import from the bare specifier:
|
|
21
|
+
*
|
|
22
|
+
* import { createAgentManager, Agent } from '@salesforce/sfdx-agent-sdk';
|
|
23
|
+
*
|
|
24
|
+
* Harness implementations import from the subpath:
|
|
25
|
+
*
|
|
26
|
+
* import { HarnessBusOwner, GenSink } from '@salesforce/sfdx-agent-sdk/harness';
|
|
27
|
+
*
|
|
28
|
+
* The split makes the boundary mechanical: a consumer who tries to import
|
|
29
|
+
* `HarnessBusOwner` from the bare specifier gets an immediate
|
|
30
|
+
* "not exported" error from the bundler / TS resolver, instead of
|
|
31
|
+
* accidentally reaching for a primitive they shouldn't be using.
|
|
32
|
+
*
|
|
33
|
+
* Type-only references to the contract interface (`AgentHarness`,
|
|
34
|
+
* `HarnessFactory`) ALSO appear on the bare specifier so consumer code
|
|
35
|
+
* that needs `AgentManager<H extends AgentHarness>` typing still works
|
|
36
|
+
* without a subpath import — but their runtime / constructible
|
|
37
|
+
* counterparts live only here.
|
|
38
|
+
*
|
|
39
|
+
* Symbols that consumers and harnesses both need (`AgentConfig`,
|
|
40
|
+
* `StreamOptions`, `DEFAULT_MAX_STEPS`, `resolveToolApprovalMode`,
|
|
41
|
+
* `ChatEvent` types, `Message` types, `MCPConfiguration`, etc.) stay on
|
|
42
|
+
* the bare specifier. The split is "harness-only" vs. "consumer-AND-harness",
|
|
43
|
+
* not "harness vs. consumer."
|
|
44
|
+
*/
|
|
45
|
+
export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './index.js';
|
|
46
|
+
export { SUPPORTED_PROTOCOL_VERSIONS } from './agent-harness.js';
|
|
47
|
+
export { HarnessBusOwner } from './harness-bus-owner.js';
|
|
48
|
+
export { lowerStreamInput, type InputMessagePart } from './stream-input.js';
|
|
49
|
+
export { GenSink } from './gen-sink.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
export { SUPPORTED_PROTOCOL_VERSIONS } from './agent-harness.js';
|
|
6
|
+
// ── Harness-implementation helpers ──────────────────────────────────
|
|
7
|
+
export { HarnessBusOwner } from './harness-bus-owner.js';
|
|
8
|
+
export { lowerStreamInput } from './stream-input.js';
|
|
9
|
+
export { GenSink } from './gen-sink.js';
|
|
10
|
+
//# sourceMappingURL=public.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,6 @@ export { type Agent } from './agent.js';
|
|
|
14
14
|
export { type ChatSession, type ChatOptions } from './chat-session.js';
|
|
15
15
|
export type { AgentConnectivityResolver, ResolvedConnectivity } from './agent-connectivity-resolver.js';
|
|
16
16
|
export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './harness/index.js';
|
|
17
|
-
export { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
|
|
18
|
-
export { HarnessBusOwner } from './harness/harness-bus-owner.js';
|
|
19
|
-
export { lowerStreamInput, type InputMessagePart } from './harness/stream-input.js';
|
|
20
17
|
export { AgentSDKError, AgentSDKErrorType } from './errors.js';
|
|
21
18
|
export type { AgentCreatedEvent, AgentDestroyedEvent, ChatStreamCompletedEvent, ChatStreamErrorEvent, ChatStreamStartedEvent, ChatStreamTrigger, McpServerDiscoveryCompletedEvent, McpServerDiscoveryFailedEvent, McpServerDiscoveryStartedEvent, McpServerStatusChangedEvent, SessionCreatedEvent, SessionDestroyedEvent, TelemetryEvent, TelemetryEventCallback, ToolApprovalRequestedEvent, ToolApprovalResolvedEvent, ToolExecutionCompletedEvent, ToolExecutionStartedEvent, } from './types/telemetry-events.js';
|
|
22
19
|
export type { LogLevel, LogRecord, Unsubscribe } from '@salesforce/agentic-common';
|
package/dist/index.js
CHANGED
|
@@ -10,9 +10,6 @@ export { inferSfApiEnv, SfApiEnv } from '@salesforce/agentic-common';
|
|
|
10
10
|
export { createAgentManager } from './agent-manager.js';
|
|
11
11
|
export {} from './agent.js';
|
|
12
12
|
export {} from './chat-session.js';
|
|
13
|
-
export { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
|
|
14
|
-
export { HarnessBusOwner } from './harness/harness-bus-owner.js';
|
|
15
|
-
export { lowerStreamInput } from './harness/stream-input.js';
|
|
16
13
|
// ── Errors ───────────────────────────────────────────────────────────
|
|
17
14
|
export { AgentSDKError, AgentSDKErrorType } from './errors.js';
|
|
18
15
|
// ── MCP Auth ────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./harness": {
|
|
14
|
+
"types": "./dist/harness/public.d.ts",
|
|
15
|
+
"default": "./dist/harness/public.js"
|
|
16
|
+
},
|
|
13
17
|
"./package.json": "./package.json"
|
|
14
18
|
},
|
|
15
19
|
"scripts": {
|
|
@@ -35,13 +39,13 @@
|
|
|
35
39
|
"LICENSE.txt"
|
|
36
40
|
],
|
|
37
41
|
"dependencies": {
|
|
38
|
-
"@salesforce/agentic-common": "0.
|
|
39
|
-
"@salesforce/llm-gateway-sdk": "0.
|
|
42
|
+
"@salesforce/agentic-common": "0.8.0",
|
|
43
|
+
"@salesforce/llm-gateway-sdk": "0.12.0"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
42
46
|
"@eslint/js": "^10.0.1",
|
|
43
|
-
"@salesforce/sfdx-agent-harness-claude": "0.
|
|
44
|
-
"@salesforce/sfdx-agent-harness-mastra": "0.
|
|
47
|
+
"@salesforce/sfdx-agent-harness-claude": "0.12.0",
|
|
48
|
+
"@salesforce/sfdx-agent-harness-mastra": "0.15.0",
|
|
45
49
|
"@types/node": "^22.19.17",
|
|
46
50
|
"@vitest/coverage-istanbul": "^4.1.7",
|
|
47
51
|
"@vitest/eslint-plugin": "^1.6.17",
|