@witqq/agent-sdk 0.6.0 → 0.7.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 +433 -6
- package/dist/auth/index.cjs +188 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +154 -138
- package/dist/auth/index.d.ts +154 -138
- package/dist/auth/index.js +188 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/backends/claude.cjs +341 -22
- package/dist/backends/claude.cjs.map +1 -1
- package/dist/backends/claude.d.cts +2 -1
- package/dist/backends/claude.d.ts +2 -1
- package/dist/backends/claude.js +341 -22
- package/dist/backends/claude.js.map +1 -1
- package/dist/backends/copilot.cjs +133 -25
- package/dist/backends/copilot.cjs.map +1 -1
- package/dist/backends/copilot.d.cts +2 -1
- package/dist/backends/copilot.d.ts +2 -1
- package/dist/backends/copilot.js +133 -25
- package/dist/backends/copilot.js.map +1 -1
- package/dist/backends/vercel-ai.cjs +66 -19
- package/dist/backends/vercel-ai.cjs.map +1 -1
- package/dist/backends/vercel-ai.d.cts +1 -1
- package/dist/backends/vercel-ai.d.ts +1 -1
- package/dist/backends/vercel-ai.js +66 -19
- package/dist/backends/vercel-ai.js.map +1 -1
- package/dist/chat/accumulator.cjs +147 -0
- package/dist/chat/accumulator.cjs.map +1 -0
- package/dist/chat/accumulator.d.cts +61 -0
- package/dist/chat/accumulator.d.ts +61 -0
- package/dist/chat/accumulator.js +145 -0
- package/dist/chat/accumulator.js.map +1 -0
- package/dist/chat/backends.cjs +3534 -0
- package/dist/chat/backends.cjs.map +1 -0
- package/dist/chat/backends.d.cts +62 -0
- package/dist/chat/backends.d.ts +62 -0
- package/dist/chat/backends.js +3501 -0
- package/dist/chat/backends.js.map +1 -0
- package/dist/chat/context.cjs +230 -0
- package/dist/chat/context.cjs.map +1 -0
- package/dist/chat/context.d.cts +167 -0
- package/dist/chat/context.d.ts +167 -0
- package/dist/chat/context.js +227 -0
- package/dist/chat/context.js.map +1 -0
- package/dist/chat/core.cjs +282 -0
- package/dist/chat/core.cjs.map +1 -0
- package/dist/chat/core.d.cts +435 -0
- package/dist/chat/core.d.ts +435 -0
- package/dist/chat/core.js +261 -0
- package/dist/chat/core.js.map +1 -0
- package/dist/chat/errors.cjs +251 -0
- package/dist/chat/errors.cjs.map +1 -0
- package/dist/chat/errors.d.cts +122 -0
- package/dist/chat/errors.d.ts +122 -0
- package/dist/chat/errors.js +243 -0
- package/dist/chat/errors.js.map +1 -0
- package/dist/chat/events.cjs +203 -0
- package/dist/chat/events.cjs.map +1 -0
- package/dist/chat/events.d.cts +241 -0
- package/dist/chat/events.d.ts +241 -0
- package/dist/chat/events.js +196 -0
- package/dist/chat/events.js.map +1 -0
- package/dist/chat/index.cjs +5359 -0
- package/dist/chat/index.cjs.map +1 -0
- package/dist/chat/index.d.cts +52 -0
- package/dist/chat/index.d.ts +52 -0
- package/dist/chat/index.js +5296 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/react.cjs +2739 -0
- package/dist/chat/react.cjs.map +1 -0
- package/dist/chat/react.d.cts +619 -0
- package/dist/chat/react.d.ts +619 -0
- package/dist/chat/react.js +2714 -0
- package/dist/chat/react.js.map +1 -0
- package/dist/chat/runtime.cjs +1030 -0
- package/dist/chat/runtime.cjs.map +1 -0
- package/dist/chat/runtime.d.cts +118 -0
- package/dist/chat/runtime.d.ts +118 -0
- package/dist/chat/runtime.js +1028 -0
- package/dist/chat/runtime.js.map +1 -0
- package/dist/chat/server.cjs +643 -0
- package/dist/chat/server.cjs.map +1 -0
- package/dist/chat/server.d.cts +287 -0
- package/dist/chat/server.d.ts +287 -0
- package/dist/chat/server.js +617 -0
- package/dist/chat/server.js.map +1 -0
- package/dist/chat/sessions.cjs +398 -0
- package/dist/chat/sessions.cjs.map +1 -0
- package/dist/chat/sessions.d.cts +239 -0
- package/dist/chat/sessions.d.ts +239 -0
- package/dist/chat/sessions.js +394 -0
- package/dist/chat/sessions.js.map +1 -0
- package/dist/chat/state.cjs +177 -0
- package/dist/chat/state.cjs.map +1 -0
- package/dist/chat/state.d.cts +92 -0
- package/dist/chat/state.d.ts +92 -0
- package/dist/chat/state.js +167 -0
- package/dist/chat/state.js.map +1 -0
- package/dist/chat/storage.cjs +240 -0
- package/dist/chat/storage.cjs.map +1 -0
- package/dist/chat/storage.d.cts +191 -0
- package/dist/chat/storage.d.ts +191 -0
- package/dist/chat/storage.js +236 -0
- package/dist/chat/storage.js.map +1 -0
- package/dist/errors-BDLbNu9w.d.cts +13 -0
- package/dist/errors-BDLbNu9w.d.ts +13 -0
- package/dist/in-process-transport-C2oPTYs6.d.ts +223 -0
- package/dist/in-process-transport-DG-w5G6k.d.cts +223 -0
- package/dist/index.cjs +25 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -4
- package/dist/index.d.ts +32 -4
- package/dist/index.js +25 -13
- package/dist/index.js.map +1 -1
- package/dist/transport-D1OaUgRk.d.ts +67 -0
- package/dist/transport-DX1Nhm4N.d.cts +67 -0
- package/dist/types-Bh5AhqD-.d.ts +141 -0
- package/dist/types-CGF7AEX1.d.cts +141 -0
- package/dist/{types-BvwNzZCj.d.cts → types-CqvUAYxt.d.cts} +21 -3
- package/dist/{types-BvwNzZCj.d.ts → types-CqvUAYxt.d.ts} +21 -3
- package/dist/types-DLZzlJxt.d.ts +39 -0
- package/dist/types-tE0CXwBl.d.cts +39 -0
- package/package.json +149 -2
package/README.md
CHANGED
|
@@ -81,6 +81,23 @@ const writeFileTool: ToolDefinition = {
|
|
|
81
81
|
|
|
82
82
|
When `needsApproval: true`, the `supervisor.onPermission` callback is invoked before execution. Without a supervisor, approval-required tools are denied by default.
|
|
83
83
|
|
|
84
|
+
Runtime-registered tools receive an optional `ToolContext` as their second parameter:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import type { ToolContext } from "@witqq/agent-sdk";
|
|
88
|
+
|
|
89
|
+
const dbTool: ToolDefinition = {
|
|
90
|
+
name: "query_db",
|
|
91
|
+
description: "Query the database",
|
|
92
|
+
parameters: z.object({ sql: z.string() }),
|
|
93
|
+
execute: async (params, context?: ToolContext) => {
|
|
94
|
+
// context.sessionId — current chat session
|
|
95
|
+
// context.custom — session metadata
|
|
96
|
+
return db.query(params.sql);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
84
101
|
## Permission Handling
|
|
85
102
|
|
|
86
103
|
The `supervisor` hooks intercept permission requests and user-facing questions:
|
|
@@ -502,19 +519,429 @@ interface CopilotAuthToken extends AuthToken {
|
|
|
502
519
|
}
|
|
503
520
|
```
|
|
504
521
|
|
|
522
|
+
### Token Auto-Refresh
|
|
523
|
+
|
|
524
|
+
`TokenRefreshManager` schedules background token refresh before expiry:
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { TokenRefreshManager } from "@witqq/agent-sdk/auth";
|
|
528
|
+
|
|
529
|
+
const manager = new TokenRefreshManager({
|
|
530
|
+
token: authToken,
|
|
531
|
+
refreshFn: async (token) => claudeAuth.refreshToken(token.refreshToken!),
|
|
532
|
+
refreshThreshold: 0.8, // refresh at 80% of token lifetime
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
manager.on("refreshed", (newToken) => { /* update stored token */ });
|
|
536
|
+
manager.on("expired", () => { /* re-authenticate */ });
|
|
537
|
+
manager.start();
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
## Chat SDK (experimental)
|
|
541
|
+
|
|
542
|
+
Higher-level primitives for building AI chat applications on top of agent-sdk.
|
|
543
|
+
|
|
544
|
+
### Barrel Import
|
|
545
|
+
|
|
546
|
+
For most consumer apps, import common types from a single path:
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import {
|
|
550
|
+
ChatMessage, ChatSession, ChatEvent, IChatRuntime,
|
|
551
|
+
createChatRuntime, ChatError, classifyError,
|
|
552
|
+
useChat, useRemoteChat, useRemoteAuth,
|
|
553
|
+
ChatProvider, Thread, Composer,
|
|
554
|
+
RemoteChatRuntime, SSEChatTransport,
|
|
555
|
+
} from "@witqq/agent-sdk/chat";
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Individual Module Imports
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { ChatMessage, ChatSession, IChatProvider, isChatMessage } from "@witqq/agent-sdk/chat/core";
|
|
562
|
+
import {
|
|
563
|
+
classifyError, withRetry, isRetryable,
|
|
564
|
+
ChatSDKError, NetworkError, RateLimitError,
|
|
565
|
+
ExponentialBackoffStrategy
|
|
566
|
+
} from "@witqq/agent-sdk/chat/errors";
|
|
567
|
+
import {
|
|
568
|
+
ChatEventBus, filterEvents, collectText
|
|
569
|
+
} from "@witqq/agent-sdk/chat/events";
|
|
570
|
+
import {
|
|
571
|
+
InMemoryStorage, FileStorage,
|
|
572
|
+
type IStorageAdapter, StorageError
|
|
573
|
+
} from "@witqq/agent-sdk/chat/storage";
|
|
574
|
+
import {
|
|
575
|
+
InMemorySessionStore, FileSessionStore,
|
|
576
|
+
type IChatSessionStore
|
|
577
|
+
} from "@witqq/agent-sdk/chat/sessions";
|
|
578
|
+
import {
|
|
579
|
+
ContextWindowManager, estimateTokens
|
|
580
|
+
} from "@witqq/agent-sdk/chat/context";
|
|
581
|
+
import {
|
|
582
|
+
CopilotChatAdapter, VercelAIChatAdapter, BaseBackendAdapter,
|
|
583
|
+
SSEChatTransport, WsChatTransport, InProcessChatTransport,
|
|
584
|
+
streamToTransport, withInterceptors,
|
|
585
|
+
type IBackendAdapter, type BackendAdapterOptions, type IChatTransport
|
|
586
|
+
} from "@witqq/agent-sdk/chat/backends";
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Error Classification
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
try {
|
|
593
|
+
await provider.send(message);
|
|
594
|
+
} catch (err) {
|
|
595
|
+
const classified = classifyError(err);
|
|
596
|
+
if (classified instanceof RateLimitError) {
|
|
597
|
+
console.log(`Rate limited, retry after ${classified.retryAfterSeconds}s`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Retry with Backoff
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
const result = await withRetry(
|
|
606
|
+
() => provider.send(message),
|
|
607
|
+
new ExponentialBackoffStrategy({ maxAttempts: 3 }),
|
|
608
|
+
{ signal: AbortSignal.timeout(30_000) },
|
|
609
|
+
);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Event Bus with Middleware
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
const bus = new ChatEventBus();
|
|
616
|
+
|
|
617
|
+
// Logging middleware
|
|
618
|
+
bus.use((ctx) => {
|
|
619
|
+
console.log(`[${ctx.event.type}]`);
|
|
620
|
+
ctx.next();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Filter out heartbeat events
|
|
624
|
+
bus.use((ctx) => {
|
|
625
|
+
if (ctx.event.type === "heartbeat") ctx.suppress();
|
|
626
|
+
else ctx.next();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
bus.on("message_delta", (event) => console.log(event.text));
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Storage Adapters
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
// In-memory (dev/testing)
|
|
636
|
+
const mem = new InMemoryStorage<ChatSession>();
|
|
637
|
+
await mem.create("s1", session);
|
|
638
|
+
const s = await mem.get("s1"); // deep copy, mutation-safe
|
|
639
|
+
|
|
640
|
+
// File-based (persistence)
|
|
641
|
+
const fs = new FileStorage<ChatSession>({ directory: "./data/sessions" });
|
|
642
|
+
await fs.create("s1", session);
|
|
643
|
+
const items = await fs.query({
|
|
644
|
+
filter: (s) => s.metadata.tags.includes("important"),
|
|
645
|
+
sort: (a, b) => b.updatedAt - a.updatedAt,
|
|
646
|
+
limit: 10,
|
|
647
|
+
});
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Session Store
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
const store = new InMemorySessionStore();
|
|
654
|
+
// or: new FileSessionStore({ directory: "./data/sessions" })
|
|
655
|
+
|
|
656
|
+
const session = await store.createSession({
|
|
657
|
+
config: { model: "gpt-4", backend: "vercel-ai" },
|
|
658
|
+
title: "Code Review",
|
|
659
|
+
tags: ["work"],
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
await store.addMessage(session.id, message);
|
|
663
|
+
const page = await store.getMessages(session.id, { limit: 20, offset: 0 });
|
|
664
|
+
// page.messages, page.total, page.hasMore
|
|
665
|
+
|
|
666
|
+
const results = await store.searchSessions({ query: "typescript" });
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Context Window Manager
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
const manager = new ContextWindowManager({
|
|
673
|
+
maxTokens: 4096,
|
|
674
|
+
reservedTokens: 500,
|
|
675
|
+
strategy: "truncate-oldest", // or "sliding-window", "summarize-placeholder"
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const result = manager.fitMessages(messages);
|
|
679
|
+
// result.messages — trimmed to fit budget
|
|
680
|
+
// result.wasTruncated — whether messages were removed
|
|
681
|
+
// result.totalTokens — estimated token usage
|
|
682
|
+
// result.removedCount — how many messages were dropped
|
|
683
|
+
|
|
684
|
+
// Async variant with optional summarizer (summarize-placeholder strategy)
|
|
685
|
+
const asyncManager = new ContextWindowManager({
|
|
686
|
+
maxTokens: 4096,
|
|
687
|
+
strategy: "summarize-placeholder",
|
|
688
|
+
summarizer: async (removed) => {
|
|
689
|
+
// Call LLM or custom logic to summarize removed messages
|
|
690
|
+
return `Summary of ${removed.length} messages: ...`;
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
const asyncResult = await asyncManager.fitMessagesAsync(messages);
|
|
694
|
+
|
|
695
|
+
// Per-message estimation
|
|
696
|
+
const tokens = estimateTokens(message); // ~chars/4
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Backend Adapters
|
|
700
|
+
|
|
701
|
+
Backend adapters bridge `IAgentService` to `IChatProvider`, adding session management and resume support:
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
import { CopilotChatAdapter } from "@witqq/agent-sdk/chat/backends";
|
|
705
|
+
|
|
706
|
+
const adapter = new CopilotChatAdapter({
|
|
707
|
+
agentConfig: {
|
|
708
|
+
systemPrompt: "You are a helpful assistant.",
|
|
709
|
+
model: "gpt-4.1",
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Stream a message (creates persistent session automatically)
|
|
714
|
+
for await (const event of adapter.streamMessage(session, "Hello")) {
|
|
715
|
+
// ChatEvent: text_delta, message_start, message_complete, tool_call_start, etc.
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Resume a previous session
|
|
719
|
+
if (adapter.canResume()) {
|
|
720
|
+
for await (const event of adapter.resume(session, adapter.backendSessionId!)) {
|
|
721
|
+
// Continues the existing conversation
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
adapter.dispose();
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
`IBackendAdapter` extends `IChatProvider` with `canResume()`, `resume()`, `backendSessionId`, and `agentService` accessor. Built-in adapters: `CopilotChatAdapter`, `ClaudeChatAdapter`, `VercelAIChatAdapter` (stateless, no resume). Create custom adapters by extending `BaseBackendAdapter`.
|
|
729
|
+
|
|
730
|
+
Service ownership: when `agentService` is passed via options, the adapter does **not** dispose it — the caller retains ownership. When omitted, the adapter creates and owns its service internally.
|
|
731
|
+
|
|
732
|
+
### Chat Transport
|
|
733
|
+
|
|
734
|
+
`IChatTransport` abstracts event delivery to clients. Three built-in implementations:
|
|
735
|
+
|
|
736
|
+
| Transport | Use case |
|
|
737
|
+
|---|---|
|
|
738
|
+
| `SSEChatTransport` | Server-Sent Events over HTTP |
|
|
739
|
+
| `WsChatTransport` | WebSocket via `WebSocketLike` abstraction |
|
|
740
|
+
| `InProcessChatTransport` | Zero-network async iterable for testing/embedded |
|
|
741
|
+
|
|
742
|
+
`streamToTransport()` pipes adapter events to any transport:
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
import { SSEChatTransport, WsChatTransport, streamToTransport } from "@witqq/agent-sdk/chat/backends";
|
|
746
|
+
|
|
747
|
+
const transport = new SSEChatTransport(res);
|
|
748
|
+
await streamToTransport(adapter.streamMessage(session, message), transport);
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
**Interceptors** wrap any transport with composable hooks (logging, metrics, rate limiting):
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
import { withInterceptors, type TransportInterceptor } from "@witqq/agent-sdk/chat/backends";
|
|
755
|
+
|
|
756
|
+
const logger: TransportInterceptor = {
|
|
757
|
+
beforeSend(event) { console.log("send:", event.type); return event; },
|
|
758
|
+
onError(err) { console.error(err); },
|
|
759
|
+
};
|
|
760
|
+
const wrapped = withInterceptors(transport, [logger]);
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
**Stream watchdog** — set `streamTimeoutMs` in runtime options to abort hanging streams:
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
const runtime = createChatRuntime({
|
|
767
|
+
streamTimeoutMs: 30_000, // abort after 30s of inactivity
|
|
768
|
+
// ...
|
|
769
|
+
});
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
See [Custom Transports](docs/chat-sdk/custom-transports.md) for the implementation guide.
|
|
773
|
+
|
|
774
|
+
### Chat Runtime
|
|
775
|
+
|
|
776
|
+
`IChatRuntime<TMetadata>` is the unified facade that orchestrates backend adapters, sessions, context trimming, streaming, and middleware. `createChatRuntime()` builds one from a config:
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
import { createChatRuntime } from "@witqq/agent-sdk/chat/runtime";
|
|
780
|
+
|
|
781
|
+
const runtime = createChatRuntime({
|
|
782
|
+
backends: {
|
|
783
|
+
copilot: () => new CopilotChatAdapter({ agentService }),
|
|
784
|
+
claude: () => new ClaudeChatAdapter({ agentService: claudeService }),
|
|
785
|
+
},
|
|
786
|
+
defaultBackend: "copilot",
|
|
787
|
+
sessionStore: new InMemorySessionStore(),
|
|
788
|
+
contextManager: new ContextWindowManager({ maxTokens: 8000 }),
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// Create session, send message, stream events
|
|
792
|
+
const session = await runtime.createSession();
|
|
793
|
+
for await (const event of runtime.send(session.id, "Hello")) {
|
|
794
|
+
console.log(event.type, event);
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
Key capabilities: session delegation (create/get/list/delete/archive/switch), backend/model switching with `switchBackend(name)` / `switchModel(model)`, tool registration via `addTool(def)` / `removeTool(name)` (persists across switches), middleware pipeline (`use(middleware)`), state machine (`status` property), abort support (`abort()`), pre-stream retry with `RetryConfig`, generic `<TMetadata>` for typed session metadata, and `dispose()`.
|
|
799
|
+
|
|
800
|
+
Context monitoring:
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
// Query context usage after send
|
|
804
|
+
const stats = runtime.getContextStats(session.id);
|
|
805
|
+
// stats: { totalTokens, removedCount, wasTruncated, availableBudget } | null
|
|
806
|
+
|
|
807
|
+
// Archive trimmed messages via callback
|
|
808
|
+
const runtime = createChatRuntime({
|
|
809
|
+
// ...backends, sessionStore, contextManager
|
|
810
|
+
onContextTrimmed: (sessionId, removedMessages) => {
|
|
811
|
+
db.archiveMessages(sessionId, removedMessages);
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### Server Utilities
|
|
817
|
+
|
|
818
|
+
Framework-agnostic HTTP handlers for serving `IChatRuntime` over HTTP. Import from `@witqq/agent-sdk/chat/server`.
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
import {
|
|
822
|
+
createChatHandler,
|
|
823
|
+
createAuthHandler,
|
|
824
|
+
FileTokenStore,
|
|
825
|
+
corsMiddleware,
|
|
826
|
+
createChatServer,
|
|
827
|
+
} from "@witqq/agent-sdk/chat/server";
|
|
828
|
+
import { createChatRuntime } from "@witqq/agent-sdk/chat/runtime";
|
|
829
|
+
|
|
830
|
+
const runtime = createChatRuntime({ /* ... */ });
|
|
831
|
+
|
|
832
|
+
// Option 1: Compose handlers manually
|
|
833
|
+
const chatHandler = createChatHandler(runtime, { prefix: "/api/chat" });
|
|
834
|
+
const authHandler = createAuthHandler({
|
|
835
|
+
tokenStore: new FileTokenStore({ directory: "./tokens" }),
|
|
836
|
+
onAuth: (backend, token) => { /* handle auth */ },
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Option 2: One-call server factory
|
|
840
|
+
const handler = createChatServer({
|
|
841
|
+
runtime,
|
|
842
|
+
cors: true,
|
|
843
|
+
staticDir: "./public",
|
|
844
|
+
});
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
`createChatHandler` maps all 10 `RemoteChatRuntime` endpoints (session CRUD, send via SSE, abort, models, backend/model switch). `createAuthHandler` handles Copilot Device Flow, Claude OAuth+PKCE, and API key auth with persistent token storage via `ITokenStore`. `corsMiddleware` supports multi-origin configuration.
|
|
848
|
+
|
|
505
849
|
## Interactive Demo
|
|
506
850
|
|
|
507
|
-
|
|
851
|
+
Single-screen chat UI with inline provider/model selection and auth.
|
|
508
852
|
|
|
509
853
|
```bash
|
|
510
|
-
#
|
|
511
|
-
|
|
854
|
+
npm run demo # Build & start in Docker (http://localhost:3456)
|
|
855
|
+
npm run demo -- stop # Stop
|
|
856
|
+
npm run demo -- logs # Follow logs
|
|
857
|
+
npm run demo -- restart # Rebuild & restart
|
|
858
|
+
npm run demo -- dev # Local dev without Docker
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
Features: inline provider switching, auth via modal dialog (Copilot Device Flow, Claude OAuth+PKCE, Vercel AI API key), model dropdown with search, SSE streaming chat with thinking blocks, tool calls, and error rendering.
|
|
862
|
+
|
|
863
|
+
## React Bindings
|
|
512
864
|
|
|
513
|
-
|
|
514
|
-
|
|
865
|
+
Headless React hooks and components for building chat UIs:
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
import { useChat, Thread, Composer, ChatProvider } from "@witqq/agent-sdk/chat/react";
|
|
869
|
+
|
|
870
|
+
function App() {
|
|
871
|
+
return (
|
|
872
|
+
<ChatProvider runtime={runtime}>
|
|
873
|
+
<Thread />
|
|
874
|
+
<Composer />
|
|
875
|
+
</ChatProvider>
|
|
876
|
+
);
|
|
877
|
+
}
|
|
515
878
|
```
|
|
516
879
|
|
|
517
|
-
|
|
880
|
+
For client-server architectures, `useRemoteChat` manages the full auth → runtime → session lifecycle:
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
import { useRemoteChat, ChatProvider, Thread, Composer } from "@witqq/agent-sdk/chat/react";
|
|
884
|
+
|
|
885
|
+
function App() {
|
|
886
|
+
const chat = useRemoteChat({
|
|
887
|
+
chatBaseUrl: "/api/chat",
|
|
888
|
+
authBaseUrl: "/api",
|
|
889
|
+
backend: "copilot",
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
if (chat.phase !== "ready" || !chat.runtime) return <div>Loading...</div>;
|
|
893
|
+
|
|
894
|
+
return (
|
|
895
|
+
<ChatProvider runtime={chat.runtime}>
|
|
896
|
+
<Thread />
|
|
897
|
+
<Composer />
|
|
898
|
+
</ChatProvider>
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
Or use `RemoteChatRuntime` directly for lower-level control:
|
|
904
|
+
|
|
905
|
+
```typescript
|
|
906
|
+
import { RemoteChatRuntime } from "@witqq/agent-sdk/chat/react";
|
|
907
|
+
|
|
908
|
+
const runtime = new RemoteChatRuntime({ baseUrl: "/api/chat" });
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
Reactive session list (replaces manual polling):
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
import { useSessions } from "@witqq/agent-sdk/chat/react";
|
|
915
|
+
|
|
916
|
+
function SessionList() {
|
|
917
|
+
const { sessions, loading } = useSessions();
|
|
918
|
+
// Auto-updates on create, delete, archive, and message send
|
|
919
|
+
return sessions.map(s => <div key={s.id}>{s.title}</div>);
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
Server-delegated authentication (no `node:crypto` in browser):
|
|
924
|
+
|
|
925
|
+
```typescript
|
|
926
|
+
import { useRemoteAuth } from "@witqq/agent-sdk/chat/react";
|
|
927
|
+
|
|
928
|
+
const auth = useRemoteAuth({ backend: "copilot", baseUrl: "/api" });
|
|
929
|
+
// auth.startDeviceFlow(), auth.startOAuthFlow(), auth.submitApiKey()
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
See [Chat SDK docs](docs/chat-sdk/README.md) for the full React API reference.
|
|
933
|
+
|
|
934
|
+
## Documentation
|
|
935
|
+
|
|
936
|
+
| Document | Description |
|
|
937
|
+
|----------|-------------|
|
|
938
|
+
| [Chat SDK Modules](docs/chat-sdk/README.md) | Module-by-module API docs for chat primitives |
|
|
939
|
+
| [Chat SDK Architecture](docs/chat-sdk/ARCHITECTURE.md) | Architecture specification and design decisions |
|
|
940
|
+
| [Custom Transports](docs/chat-sdk/custom-transports.md) | Guide to building custom IChatTransport implementations |
|
|
941
|
+
| [Custom Renderers](docs/chat-sdk/custom-renderers.md) | Three approaches to customizing React UI components |
|
|
942
|
+
| [Roadmap](docs/architecture/ROADMAP.md) | Module implementation roadmap (M1-M12) |
|
|
943
|
+
| [Project Checklist](PROJECT_CHECKLIST.md) | Implementation checklist with completion status |
|
|
944
|
+
| [Changelog](CHANGELOG.md) | Release history and breaking changes |
|
|
518
945
|
|
|
519
946
|
## License
|
|
520
947
|
|
package/dist/auth/index.cjs
CHANGED
|
@@ -2,8 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
4
|
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var AgentSDKError = class extends Error {
|
|
7
|
+
/** @internal Marker for cross-bundle identity checks */
|
|
8
|
+
_agentSDKError = true;
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = "AgentSDKError";
|
|
12
|
+
}
|
|
13
|
+
/** Check if an error is an AgentSDKError (works across bundled copies) */
|
|
14
|
+
static is(error) {
|
|
15
|
+
return error instanceof Error && "_agentSDKError" in error && error._agentSDKError === true;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
5
19
|
// src/auth/types.ts
|
|
6
|
-
var AuthError = class extends
|
|
20
|
+
var AuthError = class extends AgentSDKError {
|
|
7
21
|
constructor(message, options) {
|
|
8
22
|
super(message, options);
|
|
9
23
|
this.name = "AuthError";
|
|
@@ -349,11 +363,184 @@ function hexEncode(bytes) {
|
|
|
349
363
|
return Buffer.from(bytes).toString("hex");
|
|
350
364
|
}
|
|
351
365
|
|
|
366
|
+
// src/auth/refresh-manager.ts
|
|
367
|
+
var TokenRefreshManager = class {
|
|
368
|
+
currentToken;
|
|
369
|
+
refreshFn;
|
|
370
|
+
threshold;
|
|
371
|
+
maxRetries;
|
|
372
|
+
retryDelayMs;
|
|
373
|
+
minDelayMs;
|
|
374
|
+
timerId = null;
|
|
375
|
+
running = false;
|
|
376
|
+
disposed = false;
|
|
377
|
+
listeners = {
|
|
378
|
+
refreshed: /* @__PURE__ */ new Set(),
|
|
379
|
+
error: /* @__PURE__ */ new Set(),
|
|
380
|
+
expired: /* @__PURE__ */ new Set(),
|
|
381
|
+
disposed: /* @__PURE__ */ new Set()
|
|
382
|
+
};
|
|
383
|
+
constructor(options) {
|
|
384
|
+
this.currentToken = { ...options.token };
|
|
385
|
+
this.refreshFn = options.refresh;
|
|
386
|
+
this.threshold = options.refreshThreshold ?? 0.8;
|
|
387
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
388
|
+
this.retryDelayMs = options.retryDelayMs ?? 1e3;
|
|
389
|
+
this.minDelayMs = options.minDelayMs ?? 1e3;
|
|
390
|
+
}
|
|
391
|
+
/** Register an event listener */
|
|
392
|
+
on(event, listener) {
|
|
393
|
+
this.listeners[event].add(listener);
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
396
|
+
/** Remove an event listener */
|
|
397
|
+
off(event, listener) {
|
|
398
|
+
this.listeners[event].delete(listener);
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
/** Current token managed by this instance */
|
|
402
|
+
get token() {
|
|
403
|
+
return { ...this.currentToken };
|
|
404
|
+
}
|
|
405
|
+
/** Whether the manager is currently running */
|
|
406
|
+
get isRunning() {
|
|
407
|
+
return this.running;
|
|
408
|
+
}
|
|
409
|
+
/** Whether the manager has been disposed */
|
|
410
|
+
get isDisposed() {
|
|
411
|
+
return this.disposed;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Start automatic refresh scheduling.
|
|
415
|
+
* If the token is already expired, emits "expired" immediately.
|
|
416
|
+
* If the token has no expiresIn, does nothing (long-lived token).
|
|
417
|
+
*/
|
|
418
|
+
start() {
|
|
419
|
+
if (this.disposed) return;
|
|
420
|
+
if (this.running) return;
|
|
421
|
+
this.running = true;
|
|
422
|
+
this.schedule();
|
|
423
|
+
}
|
|
424
|
+
/** Stop automatic refresh (can be restarted with start()) */
|
|
425
|
+
stop() {
|
|
426
|
+
this.running = false;
|
|
427
|
+
this.clearTimer();
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Update the managed token (e.g. after manual refresh).
|
|
431
|
+
* Reschedules automatic refresh if running.
|
|
432
|
+
*/
|
|
433
|
+
updateToken(token) {
|
|
434
|
+
if (this.disposed) return;
|
|
435
|
+
this.currentToken = { ...token };
|
|
436
|
+
if (this.running) {
|
|
437
|
+
this.clearTimer();
|
|
438
|
+
this.schedule();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** Stop and clean up all resources */
|
|
442
|
+
dispose() {
|
|
443
|
+
if (this.disposed) return;
|
|
444
|
+
this.stop();
|
|
445
|
+
this.disposed = true;
|
|
446
|
+
this.emit("disposed");
|
|
447
|
+
for (const set of Object.values(this.listeners)) {
|
|
448
|
+
set.clear();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// ─── Private ──────────────────────────────────────────────────
|
|
452
|
+
schedule() {
|
|
453
|
+
if (!this.running || this.disposed) return;
|
|
454
|
+
const delayMs = this.computeRefreshDelay();
|
|
455
|
+
if (delayMs === null) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (delayMs <= 0) {
|
|
459
|
+
this.timerId = setTimeout(() => {
|
|
460
|
+
this.timerId = null;
|
|
461
|
+
if (!this.running || this.disposed) return;
|
|
462
|
+
void this.performRefresh();
|
|
463
|
+
}, 0);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
this.timerId = setTimeout(() => {
|
|
467
|
+
this.timerId = null;
|
|
468
|
+
if (!this.running || this.disposed) return;
|
|
469
|
+
void this.performRefresh();
|
|
470
|
+
}, Math.max(delayMs, this.minDelayMs));
|
|
471
|
+
}
|
|
472
|
+
async performRefresh(attempt = 1) {
|
|
473
|
+
if (!this.running || this.disposed) return;
|
|
474
|
+
try {
|
|
475
|
+
const newToken = await this.refreshFn(this.currentToken);
|
|
476
|
+
if (!this.running || this.disposed) return;
|
|
477
|
+
this.currentToken = { ...newToken };
|
|
478
|
+
this.emit("refreshed", newToken);
|
|
479
|
+
this.schedule();
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (!this.running || this.disposed) return;
|
|
482
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
483
|
+
this.emit("error", error, attempt);
|
|
484
|
+
if (attempt < this.maxRetries) {
|
|
485
|
+
const delay = this.retryDelayMs * Math.pow(2, attempt - 1);
|
|
486
|
+
this.timerId = setTimeout(() => {
|
|
487
|
+
this.timerId = null;
|
|
488
|
+
if (!this.running || this.disposed) return;
|
|
489
|
+
void this.performRefresh(attempt + 1);
|
|
490
|
+
}, delay);
|
|
491
|
+
} else {
|
|
492
|
+
if (this.isTokenExpired()) {
|
|
493
|
+
this.running = false;
|
|
494
|
+
this.emit("expired");
|
|
495
|
+
} else {
|
|
496
|
+
const expiresIn = this.currentToken.expiresIn;
|
|
497
|
+
if (expiresIn == null) return;
|
|
498
|
+
const expiresAt = this.currentToken.obtainedAt + expiresIn * 1e3;
|
|
499
|
+
const waitMs = Math.max(expiresAt - Date.now(), this.minDelayMs);
|
|
500
|
+
this.timerId = setTimeout(() => {
|
|
501
|
+
this.timerId = null;
|
|
502
|
+
if (!this.running || this.disposed) return;
|
|
503
|
+
void this.performRefresh();
|
|
504
|
+
}, waitMs);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
computeRefreshDelay() {
|
|
510
|
+
if (this.currentToken.expiresIn == null) return null;
|
|
511
|
+
const lifetimeMs = this.currentToken.expiresIn * 1e3;
|
|
512
|
+
const refreshAtMs = this.currentToken.obtainedAt + lifetimeMs * this.threshold;
|
|
513
|
+
const now = Date.now();
|
|
514
|
+
const delay = refreshAtMs - now;
|
|
515
|
+
return delay;
|
|
516
|
+
}
|
|
517
|
+
isTokenExpired() {
|
|
518
|
+
if (this.currentToken.expiresIn == null) return false;
|
|
519
|
+
const expiresAt = this.currentToken.obtainedAt + this.currentToken.expiresIn * 1e3;
|
|
520
|
+
return Date.now() >= expiresAt;
|
|
521
|
+
}
|
|
522
|
+
clearTimer() {
|
|
523
|
+
if (this.timerId !== null) {
|
|
524
|
+
clearTimeout(this.timerId);
|
|
525
|
+
this.timerId = null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
emit(event, ...args) {
|
|
529
|
+
for (const listener of this.listeners[event]) {
|
|
530
|
+
try {
|
|
531
|
+
listener(...args);
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
352
538
|
exports.AccessDeniedError = AccessDeniedError;
|
|
353
539
|
exports.AuthError = AuthError;
|
|
354
540
|
exports.ClaudeAuth = ClaudeAuth;
|
|
355
541
|
exports.CopilotAuth = CopilotAuth;
|
|
356
542
|
exports.DeviceCodeExpiredError = DeviceCodeExpiredError;
|
|
357
543
|
exports.TokenExchangeError = TokenExchangeError;
|
|
544
|
+
exports.TokenRefreshManager = TokenRefreshManager;
|
|
358
545
|
//# sourceMappingURL=index.cjs.map
|
|
359
546
|
//# sourceMappingURL=index.cjs.map
|