@hsb3/carbon-agui-adapter 0.1.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 +102 -0
- package/dist/adapter.d.ts +104 -0
- package/dist/adapter.js +557 -0
- package/dist/coverage.d.ts +25 -0
- package/dist/coverage.js +82 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/transport.d.ts +22 -0
- package/dist/transport.js +93 -0
- package/dist/types.d.ts +460 -0
- package/dist/types.js +5 -0
- package/dist/validate.d.ts +28 -0
- package/dist/validate.js +99 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# carbon-agui-adapter
|
|
2
|
+
|
|
3
|
+
Drives IBM **Carbon AI Chat** (`@carbon/ai-chat`) from an **AG-UI** event stream. Zero runtime deps.
|
|
4
|
+
|
|
5
|
+
- Carbon `customSendMessage` → AG-UI `RunAgentInput` (thread + history + tools + state)
|
|
6
|
+
- AG-UI events → Carbon `addMessageChunk` (`partial_item` / `complete_item` / `final_response`)
|
|
7
|
+
- Keeps thread history and shared state across turns; applies `STATE_DELTA` JSON Patches
|
|
8
|
+
- Tool calls → Carbon **chain-of-thought** steps (`message_options.chain_of_thought`) with args/result/status
|
|
9
|
+
- Cancel via Carbon's `AbortSignal`; `RUN_ERROR` throws so Carbon shows the retry UI
|
|
10
|
+
- `test/carbon-compat.ts` typechecks the adapter against the real `@carbon/ai-chat` (1.19) types
|
|
11
|
+
|
|
12
|
+
## Run
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
npm run check # typecheck + tests
|
|
17
|
+
npm run build # dist/
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createAgUiSendMessage, createSseRunner } from 'carbon-agui-adapter';
|
|
24
|
+
|
|
25
|
+
const config = {
|
|
26
|
+
messaging: {
|
|
27
|
+
customSendMessage: createAgUiSendMessage({
|
|
28
|
+
run: createSseRunner({ url: 'https://my-agent/run', headers: { authorization: 'Bearer …' } }),
|
|
29
|
+
tools: [], // AG-UI tool defs forwarded each run
|
|
30
|
+
onToolCall: (c) => ({ response_type: 'system', text: `⚙ ${c.name}` }), // optional live feedback; steps land in chain-of-thought regardless
|
|
31
|
+
onStateChange: (s) => console.log(s),
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Need history/state/reset access? Use the class:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const adapter = new CarbonAgUiAdapter({ run });
|
|
41
|
+
config.messaging.customSendMessage = adapter.sendMessage;
|
|
42
|
+
adapter.messages; adapter.state; adapter.reset();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Using `@ag-ui/client` instead of raw SSE:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { HttpAgent } from '@ag-ui/client';
|
|
49
|
+
import { fromObservable } from 'carbon-agui-adapter';
|
|
50
|
+
|
|
51
|
+
const agent = new HttpAgent({ url: 'https://my-agent/run' });
|
|
52
|
+
const run = (input, { signal }) => fromObservable(agent.run(input), signal);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Event mapping
|
|
56
|
+
|
|
57
|
+
| AG-UI | Carbon |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `TEXT_MESSAGE_CONTENT` | `partial_item` (text delta) |
|
|
60
|
+
| `TEXT_MESSAGE_END` | `complete_item` (full text) + history |
|
|
61
|
+
| `TOOL_CALL_START/ARGS/END` | history; `chain_of_thought` step (`tool_name`, `request.args`); `complete_item` if `onToolCall` returns an item |
|
|
62
|
+
| `TOOL_CALL_RESULT` | history (`role: tool`); step `response.content` + `status: success` |
|
|
63
|
+
| `STATE_SNAPSHOT` / `STATE_DELTA` | `adapter.state` + `onStateChange` |
|
|
64
|
+
| `MESSAGES_SNAPSHOT` | replaces history |
|
|
65
|
+
| `RUN_ERROR` | throws `AgUiRunError` |
|
|
66
|
+
| `RUN_FINISHED` (no outcome) / stream end / abort | `final_response` (aborted text gets `stream_stopped: true`) |
|
|
67
|
+
| `RUN_FINISHED` with `outcome.type === 'interrupt'` | `user_defined` decision item (`InterruptDecisionData`); interrupt retained for resume |
|
|
68
|
+
| `MESSAGES_SNAPSHOT` with a new assistant message | rendered as a `text` item (covers non-streaming graphs, e.g. a resume continuation) |
|
|
69
|
+
| `RUN_STARTED`, `STEP_*`, `RAW`, `CUSTOM` | `onEvent` only |
|
|
70
|
+
|
|
71
|
+
## HITL: interrupt → approve/reject/edit → resume
|
|
72
|
+
|
|
73
|
+
A LangGraph interrupt (via `ag-ui-langgraph`, `emit_interrupt_outcome=True`) arrives on
|
|
74
|
+
`RUN_FINISHED.outcome`. The adapter emits a Carbon `user_defined` item carrying
|
|
75
|
+
`InterruptDecisionData` (`kind: 'interrupt'`, `interruptId`, `message`, `action`, `args`,
|
|
76
|
+
`responseSchema`, `toolCallId`) so a host renderer can draw a decision card, and retains the
|
|
77
|
+
interrupt on `adapter.pendingInterrupt`. The host resolves it:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
await adapter.respondToInterrupt(decision, instance);
|
|
81
|
+
// decision: { type: 'approve' } | { type: 'edit', args } | { type: 'reject' }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This issues a resume run — same `threadId`, empty `messages`, one `resume[]` entry — through
|
|
85
|
+
the same runner and streams the continuation back into the conversation. The decision → wire
|
|
86
|
+
mapping (see `docs/hitl-interrupt-resume.md`):
|
|
87
|
+
|
|
88
|
+
| Decision | `resume[]` entry |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `approve` | `{ status: 'resolved', payload: { approved: true } }` |
|
|
91
|
+
| `edit` | `{ status: 'resolved', payload: { approved: true, args } }` |
|
|
92
|
+
| `reject` | `{ status: 'cancelled', payload: null }` |
|
|
93
|
+
|
|
94
|
+
Register the card with the web component's `renderUserDefinedResponse` and read
|
|
95
|
+
`state.messageItem.user_defined`; see `examples/langgraph-carbon/web/src/main.ts`.
|
|
96
|
+
|
|
97
|
+
## Notes
|
|
98
|
+
|
|
99
|
+
- Verified against `@carbon/ai-chat@1.19.0`: `PartialItemChunk` / `CompleteItemChunk` (`streaming_metadata.response_id`) / `FinalResponseChunk` (`final_response.id` = `response_id`), `ItemStreamingMetadata.stream_stopped`, `CustomSendMessageOptions.signal`, `ChainOfThoughtStep`.
|
|
100
|
+
- Runtime is dependency-free; `@carbon/ai-chat` is a devDependency only for the compat typecheck (pulls ~200 MB of Carbon peers — delete `test/carbon-compat.ts` and the devDep if you don't want that).
|
|
101
|
+
- `response_type` is a string enum in Carbon (`MessageResponseTypes`); the adapter emits the plain string `"text"`, which is the enum's runtime value. Chain-of-thought is only attached to `final_response` (no live per-step updates) — return a `system` item from `onToolCall` if you need immediate feedback.
|
|
102
|
+
- JSON Patch supports `add` / `replace` / `remove` only. Use `fast-json-patch` if your agent emits `move` / `copy` / `test`.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { AgUiContext, AgUiEvent, AgUiMessage, AgUiRunner, AgUiTool, CarbonChatInstanceLike, CarbonCustomSendMessage, CarbonGenericItem, CarbonMessageFeedbackOptions, CarbonMessageRequest, Decision, Interrupt, JsonPatchOp, RunAgentInput, RunOutcome } from './types.js';
|
|
2
|
+
export interface ToolCallInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
/** Raw JSON string, concatenated from TOOL_CALL_ARGS deltas. */
|
|
6
|
+
args: string;
|
|
7
|
+
parentMessageId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AdapterOptions {
|
|
10
|
+
run: AgUiRunner;
|
|
11
|
+
threadId?: string;
|
|
12
|
+
tools?: AgUiTool[];
|
|
13
|
+
context?: AgUiContext[];
|
|
14
|
+
initialState?: unknown;
|
|
15
|
+
forwardedProps?: unknown;
|
|
16
|
+
idGenerator?: () => string;
|
|
17
|
+
/** Every AG-UI event, before translation. */
|
|
18
|
+
onEvent?: (event: AgUiEvent) => void;
|
|
19
|
+
onStateChange?: (state: unknown) => void;
|
|
20
|
+
/** Called after each ACTIVITY_SNAPSHOT/ACTIVITY_DELTA with the reconciled progress/status content. */
|
|
21
|
+
onActivity?: (activityType: string, content: Record<string, unknown>, messageId: string) => void;
|
|
22
|
+
/** Tool calls are rendered as Carbon chain-of-thought steps on the final response. Set false to disable. */
|
|
23
|
+
chainOfThought?: boolean;
|
|
24
|
+
/** Called on TOOL_CALL_END. Optionally return a Carbon item to render immediately (e.g. a system message). */
|
|
25
|
+
onToolCall?: (call: ToolCallInfo) => CarbonGenericItem | null | void | Promise<CarbonGenericItem | null | void>;
|
|
26
|
+
/**
|
|
27
|
+
* Called once when RUN_FINISHED is processed, with its `outcome` and any legacy
|
|
28
|
+
* top-level `result`. A `success` outcome carries no renderable payload, and a
|
|
29
|
+
* `result` has no obvious Carbon target — this callback is how a host observes
|
|
30
|
+
* a returned value instead of being blind to it. The interrupt path renders its
|
|
31
|
+
* own decision card; this still fires so a host sees every completion.
|
|
32
|
+
*/
|
|
33
|
+
onRunFinished?: (info: {
|
|
34
|
+
outcome?: RunOutcome;
|
|
35
|
+
result?: unknown;
|
|
36
|
+
}) => void;
|
|
37
|
+
/**
|
|
38
|
+
* When set, Carbon feedback (thumbs) config attached to each assistant TEXT
|
|
39
|
+
* response item so the UI renders the controls. Feedback attaches per-item
|
|
40
|
+
* (`message_item_options.feedback`), never per-response, and only to text
|
|
41
|
+
* items — not decision cards, tool renders, or CUSTOM-dispatched items.
|
|
42
|
+
* Capturing the click is Carbon→host via the bus and out of this adapter's scope.
|
|
43
|
+
*/
|
|
44
|
+
feedback?: CarbonMessageFeedbackOptions;
|
|
45
|
+
/**
|
|
46
|
+
* Runtime validation at the AG-UI/Carbon trust boundaries (issue gmb9).
|
|
47
|
+
* Default TRUE: incoming runner events are validated against the AG-UI
|
|
48
|
+
* protocol schema and outgoing CUSTOM Carbon items against the known-type +
|
|
49
|
+
* required-field checks; malformed ones are skipped (never thrown), so one
|
|
50
|
+
* bad event cannot kill the stream. Set false to skip validation entirely
|
|
51
|
+
* (blind cast for events, allowlist-only for items — the pre-gmb9 behavior).
|
|
52
|
+
*/
|
|
53
|
+
validate?: boolean;
|
|
54
|
+
/** Called when an incoming AG-UI event fails schema validation (then skipped). */
|
|
55
|
+
onInvalidEvent?: (raw: unknown, error: string) => void;
|
|
56
|
+
/** Called when an outgoing CUSTOM Carbon item fails validation (then skipped). */
|
|
57
|
+
onInvalidItem?: (raw: unknown, reason: string) => void;
|
|
58
|
+
}
|
|
59
|
+
export declare class AgUiRunError extends Error {
|
|
60
|
+
readonly code?: string | undefined;
|
|
61
|
+
constructor(message: string, code?: string | undefined);
|
|
62
|
+
}
|
|
63
|
+
export declare class CarbonAgUiAdapter {
|
|
64
|
+
private readonly opts;
|
|
65
|
+
readonly threadId: string;
|
|
66
|
+
state: unknown;
|
|
67
|
+
/** Progress/status state per messageId, reconciled from ACTIVITY_SNAPSHOT/ACTIVITY_DELTA. */
|
|
68
|
+
activities: Map<string, {
|
|
69
|
+
activityType: string;
|
|
70
|
+
content: Record<string, unknown>;
|
|
71
|
+
}>;
|
|
72
|
+
messages: AgUiMessage[];
|
|
73
|
+
/** The interrupt awaiting a decision, or undefined when none is pending. */
|
|
74
|
+
pendingInterrupt?: Interrupt;
|
|
75
|
+
private readonly genId;
|
|
76
|
+
/** The Carbon instance from the most recent run, reused for resume streaming. */
|
|
77
|
+
private lastInstance?;
|
|
78
|
+
constructor(opts: AdapterOptions);
|
|
79
|
+
/** Pass as `messaging.customSendMessage` in Carbon's PublicConfig. */
|
|
80
|
+
readonly sendMessage: CarbonCustomSendMessage;
|
|
81
|
+
buildRunInput(request: CarbonMessageRequest): RunAgentInput;
|
|
82
|
+
reset(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Resume the interrupted run with the user's decision. Builds a resume
|
|
85
|
+
* `RunAgentInput` per docs/hitl-interrupt-resume.md (same threadId, empty
|
|
86
|
+
* messages, one `resume[]` entry), runs it through the same runner, and
|
|
87
|
+
* streams the continuation back into the conversation.
|
|
88
|
+
*
|
|
89
|
+
* @param instance The Carbon instance to stream into. Defaults to the one
|
|
90
|
+
* from the most recent run (e.g. the interrupted turn).
|
|
91
|
+
*/
|
|
92
|
+
respondToInterrupt(decision: Decision, instance?: CarbonChatInstanceLike): Promise<void>;
|
|
93
|
+
private upsertMessage;
|
|
94
|
+
private handle;
|
|
95
|
+
/**
|
|
96
|
+
* Run one `RunAgentInput` through the runner and translate its AG-UI events
|
|
97
|
+
* into Carbon chunks on `instance`. Shared by the initial turn and by
|
|
98
|
+
* `respondToInterrupt`, so a resumed continuation streams identically.
|
|
99
|
+
*/
|
|
100
|
+
private runAndStream;
|
|
101
|
+
}
|
|
102
|
+
/** Convenience: returns just the customSendMessage function. */
|
|
103
|
+
export declare function createAgUiSendMessage(opts: AdapterOptions): CarbonCustomSendMessage;
|
|
104
|
+
export declare function applyJsonPatch<T>(doc: T, ops: JsonPatchOp[]): T;
|