@yushaw/sanqian-sdk 0.3.27 → 0.3.29
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 +557 -0
- package/dist/index.browser.d.mts +332 -7
- package/dist/index.browser.d.ts +332 -7
- package/dist/index.browser.js +1165 -36
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1165 -36
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +335 -8
- package/dist/index.d.ts +335 -8
- package/dist/index.js +1178 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1179 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ Sanqian provides two ways to integrate with external applications:
|
|
|
15
15
|
| **HTTP API** | Simple chat, any language | REST + SSE |
|
|
16
16
|
| **SDK** | Tool registration, agents, context injection, deep integration | WebSocket |
|
|
17
17
|
|
|
18
|
+
> Documentation baseline: `@yushaw/sanqian-sdk@0.3.28`
|
|
19
|
+
|
|
18
20
|
## Quick Start
|
|
19
21
|
|
|
20
22
|
### HTTP API
|
|
@@ -60,6 +62,15 @@ const sdk = new SanqianSDK({
|
|
|
60
62
|
|
|
61
63
|
---
|
|
62
64
|
|
|
65
|
+
## Recommended Reading Paths
|
|
66
|
+
|
|
67
|
+
- Tool integration first-time setup: `Tools` -> `Connection` -> `Browser Build` (if needed)
|
|
68
|
+
- Custom agent workflows: `Agents` -> `Chat` -> `Conversations`
|
|
69
|
+
- Guardrail/intervention flows: `Hooks` -> `Human-in-the-Loop` -> `Security Levels`
|
|
70
|
+
- Channel/chatbot integrations: `Messaging Channels API` -> `Capability Discovery`
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
63
74
|
## Tools
|
|
64
75
|
|
|
65
76
|
Tools are the primary way your app provides capabilities to Sanqian. When a user asks a question, the AI agent can call your tools to get data or perform actions.
|
|
@@ -341,6 +352,29 @@ const response = await sdk.chat('agent', messages, {
|
|
|
341
352
|
})
|
|
342
353
|
```
|
|
343
354
|
|
|
355
|
+
### Local file ingest
|
|
356
|
+
|
|
357
|
+
For same-machine deployments where the Sanqian backend can read a local path directly, use `uploadFile()` first, then pass the returned `file.path` into `chat()`:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
const uploaded = await sdk.uploadFile('/absolute/path/report.pdf', {
|
|
361
|
+
conversationId,
|
|
362
|
+
autoIndex: true,
|
|
363
|
+
asyncProcess: true,
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
const response = await sdk.chat('agent', messages, {
|
|
367
|
+
conversationId: uploaded.conversationId,
|
|
368
|
+
filePaths: [uploaded.file.path],
|
|
369
|
+
})
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`uploadFile(localPath)` is not a remote file transfer protocol. For remote/backend-on-another-machine cases, continue using explicit byte upload APIs.
|
|
373
|
+
|
|
374
|
+
Conversation targeting contract:
|
|
375
|
+
- If `conversationId` is omitted, backend may create a new SDK conversation and return it in `uploaded.conversationId`.
|
|
376
|
+
- If `conversationId` is provided but does not exist, `uploadFile()` throws `SanqianSDKError` with code `CONVERSATION_NOT_FOUND`.
|
|
377
|
+
|
|
344
378
|
---
|
|
345
379
|
|
|
346
380
|
## Human-in-the-Loop (HITL)
|
|
@@ -581,6 +615,239 @@ sdk.on('resourceRemoved', (resourceId) => {
|
|
|
581
615
|
|
|
582
616
|
---
|
|
583
617
|
|
|
618
|
+
## Hooks (Available)
|
|
619
|
+
|
|
620
|
+
> **Status**: Remote hooks are available in the SDK.
|
|
621
|
+
> **Compatibility**: Hook callbacks require a Sanqian backend that supports `register_hook` / `hook_invoke`. On older backends, hook registration may return ack errors and callbacks will not be invoked.
|
|
622
|
+
|
|
623
|
+
Register hooks to observe or intervene in agent execution. Unlike tools (which the agent calls) and context providers (which inject data), hooks intercept the agent's internal lifecycle -- including prompt ingress, sub-agent lifecycle, model/tool boundaries, and finalization.
|
|
624
|
+
|
|
625
|
+
### Hook Points
|
|
626
|
+
|
|
627
|
+
| Hook Point | Type | Description |
|
|
628
|
+
|-----------|------|-------------|
|
|
629
|
+
| `on_user_prompt_submit` | Intervention | User message ingress (can block or rewrite prompt) |
|
|
630
|
+
| `on_stop_failure` | Observation | Run failure classification callback |
|
|
631
|
+
| `on_subagent_start` | Intervention | Before sub-agent starts (can rewrite payload / block) |
|
|
632
|
+
| `on_subagent_stop` | Observation | Sub-agent completion callback |
|
|
633
|
+
| `on_run_start` | Observation | Agent run begins |
|
|
634
|
+
| `on_run_end` | Observation | Agent run ends (success/error) |
|
|
635
|
+
| `on_loop` | Intervention | Each planner loop iteration |
|
|
636
|
+
| `before_model` | Intervention | Before LLM call (can modify prompt) |
|
|
637
|
+
| `after_model` | Intervention | After LLM call (can modify response) |
|
|
638
|
+
| `before_tool` | Intervention | Before tool execution (can block/modify args) |
|
|
639
|
+
| `after_tool` | Intervention | After tool execution (can modify result) |
|
|
640
|
+
| `before_compress` | Intervention | Before context compression |
|
|
641
|
+
| `before_finalize` | Intervention | Before final response is persisted |
|
|
642
|
+
|
|
643
|
+
### Register Hooks
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
const sdk = new SanqianSDK({
|
|
647
|
+
appName: 'my-guardrail',
|
|
648
|
+
requestedSecurityLevel: 'elevated', // required for before_tool/after_model/before_finalize
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
// Observe all tool calls (no intervention)
|
|
652
|
+
sdk.registerHook({
|
|
653
|
+
hookPoints: ['after_tool'],
|
|
654
|
+
priority: 90,
|
|
655
|
+
}, async (ctx, payload) => {
|
|
656
|
+
console.log(`Tool ${payload.tool_name}: ${payload.result_status}`)
|
|
657
|
+
return null // no intervention
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
// Block dangerous tools
|
|
661
|
+
const hookId = sdk.registerHook({
|
|
662
|
+
hookPoints: ['before_tool'],
|
|
663
|
+
matcher: {
|
|
664
|
+
tool_names: ['delete_file', 'run_bash_command'],
|
|
665
|
+
args_pattern: { command: 'rm *' }, // optional glob matching on payload fields
|
|
666
|
+
},
|
|
667
|
+
priority: 10,
|
|
668
|
+
failPolicy: 'closed', // if hook is unreachable, block the tool
|
|
669
|
+
}, async (ctx, payload) => {
|
|
670
|
+
if (isDangerous(payload.tool_args)) {
|
|
671
|
+
return { decision: 'deny', reason: 'Blocked by safety check' }
|
|
672
|
+
}
|
|
673
|
+
return null // allow
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
// Modify tool args (e.g., inject API key)
|
|
677
|
+
sdk.registerHook({
|
|
678
|
+
hookPoints: ['before_tool'],
|
|
679
|
+
matcher: { tool_names: ['web_search'] },
|
|
680
|
+
priority: 40,
|
|
681
|
+
}, async (ctx, payload) => {
|
|
682
|
+
return {
|
|
683
|
+
value: { ...payload.tool_args, api_key: process.env.SEARCH_KEY },
|
|
684
|
+
}
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
await sdk.connect()
|
|
688
|
+
|
|
689
|
+
// Unregister at runtime
|
|
690
|
+
await sdk.unregisterHook(hookId)
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Notes:
|
|
694
|
+
- `registerHook()` returns `hookId` immediately and stores registration locally.
|
|
695
|
+
- If already connected, SDK sends `register_hook` asynchronously; transport/ack failures are reported as SDK warnings (non-throwing).
|
|
696
|
+
- For deterministic startup behavior, register hooks before the first `connect()` so declarations are included in initial app registration.
|
|
697
|
+
- `unregisterHook()` is async; use `await` when you need unregister completion before continuing.
|
|
698
|
+
- `unregisterHookAndWait()` waits for `unregister_hook_ack`, throws on nack/timeout, and restores local hook state on failure.
|
|
699
|
+
- `getRegisteredHooks()` returns a defensive snapshot of local hook registrations for diagnostics/audit UIs.
|
|
700
|
+
- `getHookExecutionStats()` returns aggregated runtime hook execution metrics (invocation/success/failure/latency/last error).
|
|
701
|
+
|
|
702
|
+
Deterministic runtime registration:
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
const hookId = await sdk.registerHookAndWait({
|
|
706
|
+
hookPoints: ['before_tool'],
|
|
707
|
+
failPolicy: 'closed',
|
|
708
|
+
}, async () => null)
|
|
709
|
+
|
|
710
|
+
await sdk.unregisterHookAndWait(hookId)
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
- `registerHookAndWait()` waits for `register_hook_ack`.
|
|
714
|
+
- On nack/timeout, it throws and rolls back local registration state.
|
|
715
|
+
- Use this when your app must fail fast on policy wiring errors.
|
|
716
|
+
- `unregisterHookAndWait()` provides the same deterministic behavior for hook teardown.
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
console.table(sdk.getRegisteredHooks().map((item) => ({
|
|
720
|
+
hookId: item.hookId,
|
|
721
|
+
hookPoints: item.registration.hookPoints.join(","),
|
|
722
|
+
failPolicy: item.registration.failPolicy || "open",
|
|
723
|
+
})))
|
|
724
|
+
|
|
725
|
+
console.table(sdk.getHookExecutionStats().map((item) => ({
|
|
726
|
+
hookId: item.hookId,
|
|
727
|
+
invocations: item.invocationCount,
|
|
728
|
+
success: item.successCount,
|
|
729
|
+
failure: item.failureCount,
|
|
730
|
+
avgMs: Number(item.averageDurationMs.toFixed(2)),
|
|
731
|
+
lastError: item.lastError || "",
|
|
732
|
+
})))
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Typed single-point registration helper:
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
sdk.registerHookForPoint('before_tool', {
|
|
739
|
+
matcher: { tool_names: ['run_bash_command'] },
|
|
740
|
+
}, async (_ctx, payload) => {
|
|
741
|
+
// payload is strongly typed for before_tool
|
|
742
|
+
return { metadata: { tool: payload.tool_name } }
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
await sdk.registerHookForPointAndWait('before_tool', {
|
|
746
|
+
failPolicy: 'closed',
|
|
747
|
+
}, async (_ctx, payload) => {
|
|
748
|
+
return { decision: payload.tool_name === 'delete_file' ? 'ask' : 'passthrough' }
|
|
749
|
+
})
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
`payloadValidation` option for typed helpers:
|
|
753
|
+
- `strict` (default): schema mismatch causes hook invocation failure.
|
|
754
|
+
- `warn`: emits SDK `error` event but still calls your handler.
|
|
755
|
+
- `off`: disables runtime schema checks.
|
|
756
|
+
|
|
757
|
+
Hook roundtrip smoke script:
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
npm --prefix packages/sdk run test:hook-roundtrip-smoke
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
This script starts a local mock WS backend and verifies all 13 hook points, strict typed payload validation failure path, deterministic unregister (`unregisterHookAndWait`) success/rollback paths, and `hookLifecycle` event emission.
|
|
764
|
+
|
|
765
|
+
### Hook Actions
|
|
766
|
+
|
|
767
|
+
Hook handlers return `null` (no intervention) or a `HookResult`:
|
|
768
|
+
|
|
769
|
+
```typescript
|
|
770
|
+
interface HookResult {
|
|
771
|
+
skip?: boolean // Block the operation (e.g., prevent tool execution)
|
|
772
|
+
value?: unknown // Replace the value (e.g., modified args or response)
|
|
773
|
+
suspend?: boolean // Pause agent, wait for human confirmation
|
|
774
|
+
suspend_payload?: unknown // Data shown in the HITL confirmation UI
|
|
775
|
+
metadata?: Record<string, unknown>
|
|
776
|
+
decision?: 'deny' | 'ask' | 'passthrough' // Structured policy decision
|
|
777
|
+
reason?: string // Human-readable reason for deny/ask
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
Decision precedence on backend aggregation: `deny > ask > passthrough`.
|
|
782
|
+
|
|
783
|
+
### Suspend (Human-in-the-Loop)
|
|
784
|
+
|
|
785
|
+
Hooks can pause the agent and wait for external confirmation:
|
|
786
|
+
|
|
787
|
+
```typescript
|
|
788
|
+
sdk.registerHook({
|
|
789
|
+
hookPoints: ['before_tool'],
|
|
790
|
+
matcher: { tool_names: ['send_email'] },
|
|
791
|
+
priority: 10,
|
|
792
|
+
}, async (ctx, payload) => {
|
|
793
|
+
// After human confirms, the hook replays with ctx.resume_value
|
|
794
|
+
if (ctx.resume_value) {
|
|
795
|
+
return ctx.resume_value.approved ? null : { skip: true }
|
|
796
|
+
}
|
|
797
|
+
// First trigger: suspend and show confirmation UI
|
|
798
|
+
return {
|
|
799
|
+
suspend: true,
|
|
800
|
+
suspend_payload: {
|
|
801
|
+
question: `Allow sending email to ${payload.tool_args.to}?`,
|
|
802
|
+
preview: JSON.stringify(payload.tool_args).substring(0, 500),
|
|
803
|
+
},
|
|
804
|
+
}
|
|
805
|
+
})
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Security Levels
|
|
809
|
+
|
|
810
|
+
Hook points require minimum security levels:
|
|
811
|
+
|
|
812
|
+
| Level | Hook Points |
|
|
813
|
+
|-------|-------------|
|
|
814
|
+
| `standard` | on_user_prompt_submit, on_stop_failure, on_subagent_start, on_subagent_stop, on_run_start, on_run_end, on_loop, after_tool, before_compress |
|
|
815
|
+
| `elevated` | before_tool, after_model, before_finalize |
|
|
816
|
+
| `unrestricted` | before_model |
|
|
817
|
+
|
|
818
|
+
Set `requestedSecurityLevel` in SDKConfig to match the hook points you need.
|
|
819
|
+
|
|
820
|
+
### Reliability
|
|
821
|
+
|
|
822
|
+
- **Fail policy**: `"open"` (default) -- hook timeout/error does not block agent. `"closed"` -- hook failure blocks the operation (use for safety-critical hooks).
|
|
823
|
+
- **Timeout**: Differentiated by hook point (tool: 10s, model: 30s, observation: 5s). Override per-hook with `timeout` in registration.
|
|
824
|
+
- **Circuit breaker**: After 5 consecutive failures, the hook is temporarily bypassed (60s recovery).
|
|
825
|
+
|
|
826
|
+
### Hook Observability Events
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
sdk.on('hookInvoked', (event) => {
|
|
830
|
+
console.log(event.hookPoint, event.callId, event.payload)
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
sdk.on('hookResult', (event) => {
|
|
834
|
+
console.log(event.hookPoint, event.success, event.durationMs, event.error)
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
sdk.on('hookLifecycle', (event) => {
|
|
838
|
+
console.log(
|
|
839
|
+
event.action,
|
|
840
|
+
event.hookId,
|
|
841
|
+
event.success,
|
|
842
|
+
event.deterministic,
|
|
843
|
+
event.durationMs,
|
|
844
|
+
event.error
|
|
845
|
+
)
|
|
846
|
+
})
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
584
851
|
## Capability Discovery
|
|
585
852
|
|
|
586
853
|
Query Sanqian's full capability registry: tools, skills, agents, and context providers.
|
|
@@ -755,6 +1022,7 @@ const sdk = new SanqianSDK({
|
|
|
755
1022
|
|
|
756
1023
|
// Display
|
|
757
1024
|
displayName: 'My App', // Shown in Sanqian UI
|
|
1025
|
+
requestedSecurityLevel: 'standard', // Optional: standard | elevated | unrestricted
|
|
758
1026
|
|
|
759
1027
|
// Launch
|
|
760
1028
|
launchCommand: '/path/to/app', // For Sanqian to start your app
|
|
@@ -777,6 +1045,12 @@ const sdk = new SanqianSDK({
|
|
|
777
1045
|
// Debug
|
|
778
1046
|
debug: false, // Console logging (default: false)
|
|
779
1047
|
|
|
1048
|
+
// Register handshake (optional; defaults shown)
|
|
1049
|
+
protocolVersion: '2026-03-29', // Register protocol version (YYYY-MM-DD)
|
|
1050
|
+
hookCapabilities: { // Hook capability declarations
|
|
1051
|
+
streamingHook: false, // Negotiated with backend (most-restrictive-wins)
|
|
1052
|
+
},
|
|
1053
|
+
|
|
780
1054
|
// Browser mode (see Browser Build section)
|
|
781
1055
|
connectionInfo: undefined, // Pre-configured connection (skips file discovery)
|
|
782
1056
|
})
|
|
@@ -925,6 +1199,7 @@ console.log(data.message.content)
|
|
|
925
1199
|
## Built-in Tools Reference
|
|
926
1200
|
|
|
927
1201
|
These tools are available to all agents. Use tool names in agent config to enable specific ones, or `["*"]` for all.
|
|
1202
|
+
This table is a snapshot. Use `sdk.listTools('builtin')` (or `/api/capabilities`) as runtime source of truth.
|
|
928
1203
|
|
|
929
1204
|
### File Operations
|
|
930
1205
|
|
|
@@ -1029,6 +1304,8 @@ Sanqian 提供两种外部集成方式:
|
|
|
1029
1304
|
| **HTTP API** | 简单对话,任意语言 | REST + SSE |
|
|
1030
1305
|
| **SDK** | 工具注册、Agent、上下文注入、深度集成 | WebSocket |
|
|
1031
1306
|
|
|
1307
|
+
> 文档基线:`@yushaw/sanqian-sdk@0.3.28`
|
|
1308
|
+
|
|
1032
1309
|
## 快速开始
|
|
1033
1310
|
|
|
1034
1311
|
### HTTP API
|
|
@@ -1074,6 +1351,15 @@ const sdk = new SanqianSDK({
|
|
|
1074
1351
|
|
|
1075
1352
|
---
|
|
1076
1353
|
|
|
1354
|
+
## 推荐阅读路径
|
|
1355
|
+
|
|
1356
|
+
- 第一次做工具集成:`工具 (Tools)` -> `连接` -> `浏览器构建`(如需要)
|
|
1357
|
+
- 自定义 Agent 工作流:`Agent` -> `对话 (Chat)` -> `会话管理`
|
|
1358
|
+
- 安全干预/护栏:`Hooks` -> `人机协作 (HITL)` -> `安全等级`
|
|
1359
|
+
- 渠道机器人接入:`消息渠道 API` -> `能力发现`
|
|
1360
|
+
|
|
1361
|
+
---
|
|
1362
|
+
|
|
1077
1363
|
## 工具 (Tools)
|
|
1078
1364
|
|
|
1079
1365
|
工具是你的应用向 Sanqian 提供能力的主要方式。当用户提问时,AI Agent 可以调用你的工具来获取数据或执行操作。
|
|
@@ -1328,6 +1614,29 @@ const response = await sdk.chat('agent', messages, {
|
|
|
1328
1614
|
})
|
|
1329
1615
|
```
|
|
1330
1616
|
|
|
1617
|
+
### 本地文件导入
|
|
1618
|
+
|
|
1619
|
+
对于 Sanqian 后端与调用方在同一台机器、且后端能直接读取本地路径的场景,先调用 `uploadFile()`,再把返回的 `file.path` 传给 `chat()`:
|
|
1620
|
+
|
|
1621
|
+
```typescript
|
|
1622
|
+
const uploaded = await sdk.uploadFile('/absolute/path/report.pdf', {
|
|
1623
|
+
conversationId,
|
|
1624
|
+
autoIndex: true,
|
|
1625
|
+
asyncProcess: true,
|
|
1626
|
+
})
|
|
1627
|
+
|
|
1628
|
+
const response = await sdk.chat('agent', messages, {
|
|
1629
|
+
conversationId: uploaded.conversationId,
|
|
1630
|
+
filePaths: [uploaded.file.path],
|
|
1631
|
+
})
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
`uploadFile(localPath)` 不是远程文件传输协议。对于远程 backend 或不同机器场景,仍应使用显式字节上传能力。
|
|
1635
|
+
|
|
1636
|
+
会话目标契约:
|
|
1637
|
+
- 未传 `conversationId` 时,后端可能创建新的 SDK conversation,并通过 `uploaded.conversationId` 返回。
|
|
1638
|
+
- 传了 `conversationId` 但该会话不存在时,`uploadFile()` 会抛出 `SanqianSDKError`,错误码为 `CONVERSATION_NOT_FOUND`。
|
|
1639
|
+
|
|
1331
1640
|
---
|
|
1332
1641
|
|
|
1333
1642
|
## 人机协作 (HITL)
|
|
@@ -1506,6 +1815,239 @@ for await (const event of sdk.chatStream('agent', messages, {
|
|
|
1506
1815
|
|
|
1507
1816
|
---
|
|
1508
1817
|
|
|
1818
|
+
## Hooks(已可用)
|
|
1819
|
+
|
|
1820
|
+
> **状态**: 远程 Hook API 已在 SDK 中可用。
|
|
1821
|
+
> **兼容性**: Hook 回调依赖后端支持 `register_hook` / `hook_invoke`。旧版后端可能返回 ack 错误,且不会触发回调。
|
|
1822
|
+
|
|
1823
|
+
注册 hooks 来观察或干预 agent 执行。与工具(agent 调用)和上下文提供者(注入数据)不同,hooks 拦截 agent 的内部生命周期,包括用户输入入口、子 agent 生命周期、模型/工具边界和最终收口阶段。
|
|
1824
|
+
|
|
1825
|
+
### Hook 点
|
|
1826
|
+
|
|
1827
|
+
| Hook 点 | 类型 | 说明 |
|
|
1828
|
+
|---------|------|------|
|
|
1829
|
+
| `on_user_prompt_submit` | 干预 | 用户消息入口(可拒绝/改写) |
|
|
1830
|
+
| `on_stop_failure` | 观察 | 运行失败分类回调 |
|
|
1831
|
+
| `on_subagent_start` | 干预 | 子 agent 启动前(可改写 payload / 阻断) |
|
|
1832
|
+
| `on_subagent_stop` | 观察 | 子 agent 结束回调 |
|
|
1833
|
+
| `on_run_start` | 观察 | Agent run 开始 |
|
|
1834
|
+
| `on_run_end` | 观察 | Agent run 结束(成功/错误) |
|
|
1835
|
+
| `on_loop` | 干预 | 每轮 planner 循环 |
|
|
1836
|
+
| `before_model` | 干预 | LLM 调用前(可修改 prompt) |
|
|
1837
|
+
| `after_model` | 干预 | LLM 调用后(可修改响应) |
|
|
1838
|
+
| `before_tool` | 干预 | 工具执行前(可阻断/修改参数) |
|
|
1839
|
+
| `after_tool` | 干预 | 工具执行后(可修改结果) |
|
|
1840
|
+
| `before_compress` | 干预 | 上下文压缩前 |
|
|
1841
|
+
| `before_finalize` | 干预 | 最终响应持久化前 |
|
|
1842
|
+
|
|
1843
|
+
### 注册 Hooks
|
|
1844
|
+
|
|
1845
|
+
```typescript
|
|
1846
|
+
const sdk = new SanqianSDK({
|
|
1847
|
+
appName: 'my-guardrail',
|
|
1848
|
+
requestedSecurityLevel: 'elevated', // before_tool/after_model/before_finalize 需要
|
|
1849
|
+
})
|
|
1850
|
+
|
|
1851
|
+
// 观察所有工具调用(不干预)
|
|
1852
|
+
sdk.registerHook({
|
|
1853
|
+
hookPoints: ['after_tool'],
|
|
1854
|
+
priority: 90,
|
|
1855
|
+
}, async (ctx, payload) => {
|
|
1856
|
+
console.log(`工具 ${payload.tool_name}: ${payload.result_status}`)
|
|
1857
|
+
return null // 不干预
|
|
1858
|
+
})
|
|
1859
|
+
|
|
1860
|
+
// 阻断危险工具
|
|
1861
|
+
const hookId = sdk.registerHook({
|
|
1862
|
+
hookPoints: ['before_tool'],
|
|
1863
|
+
matcher: {
|
|
1864
|
+
tool_names: ['delete_file', 'run_bash_command'],
|
|
1865
|
+
args_pattern: { command: 'rm *' }, // 可选:按 payload 字段做 glob 匹配
|
|
1866
|
+
},
|
|
1867
|
+
priority: 10,
|
|
1868
|
+
failPolicy: 'closed', // hook 不可达时阻断操作
|
|
1869
|
+
}, async (ctx, payload) => {
|
|
1870
|
+
if (isDangerous(payload.tool_args)) {
|
|
1871
|
+
return { decision: 'deny', reason: '安全检查未通过' }
|
|
1872
|
+
}
|
|
1873
|
+
return null // 放行
|
|
1874
|
+
})
|
|
1875
|
+
|
|
1876
|
+
// 修改工具参数(如注入 API key)
|
|
1877
|
+
sdk.registerHook({
|
|
1878
|
+
hookPoints: ['before_tool'],
|
|
1879
|
+
matcher: { tool_names: ['web_search'] },
|
|
1880
|
+
priority: 40,
|
|
1881
|
+
}, async (ctx, payload) => {
|
|
1882
|
+
return {
|
|
1883
|
+
value: { ...payload.tool_args, api_key: process.env.SEARCH_KEY },
|
|
1884
|
+
}
|
|
1885
|
+
})
|
|
1886
|
+
|
|
1887
|
+
await sdk.connect()
|
|
1888
|
+
|
|
1889
|
+
// 运行时注销
|
|
1890
|
+
await sdk.unregisterHook(hookId)
|
|
1891
|
+
```
|
|
1892
|
+
|
|
1893
|
+
说明:
|
|
1894
|
+
- `registerHook()` 会立即返回 `hookId`,并先在本地登记。
|
|
1895
|
+
- 若当前已连接,SDK 会异步发送 `register_hook`;传输/ack 失败仅记录 warning(不会抛出异常)。
|
|
1896
|
+
- 若希望启动阶段行为可预测,建议在第一次 `connect()` 前完成 hook 注册,这样会随应用初始注册一并下发。
|
|
1897
|
+
- `unregisterHook()` 是异步方法;若后续逻辑依赖注销完成,请使用 `await`。
|
|
1898
|
+
- `unregisterHookAndWait()` 会等待 `unregister_hook_ack`;nack/超时时抛错,并回滚本地 hook 状态。
|
|
1899
|
+
- `getRegisteredHooks()` 返回本地 hook 注册的防御性快照,可用于诊断/审计界面。
|
|
1900
|
+
- `getHookExecutionStats()` 返回运行时 hook 执行聚合指标(调用/成功/失败/耗时/最近错误)。
|
|
1901
|
+
|
|
1902
|
+
需要确定性注册时:
|
|
1903
|
+
|
|
1904
|
+
```typescript
|
|
1905
|
+
const hookId = await sdk.registerHookAndWait({
|
|
1906
|
+
hookPoints: ['before_tool'],
|
|
1907
|
+
failPolicy: 'closed',
|
|
1908
|
+
}, async () => null)
|
|
1909
|
+
|
|
1910
|
+
await sdk.unregisterHookAndWait(hookId)
|
|
1911
|
+
```
|
|
1912
|
+
|
|
1913
|
+
- `registerHookAndWait()` 会等待 `register_hook_ack`。
|
|
1914
|
+
- 若 nack/超时,会抛错并回滚本地注册状态。
|
|
1915
|
+
- 适用于“策略挂载失败就应立即失败”的场景。
|
|
1916
|
+
- `unregisterHookAndWait()` 为 hook 下线提供同样的确定性保障。
|
|
1917
|
+
|
|
1918
|
+
```typescript
|
|
1919
|
+
console.table(sdk.getRegisteredHooks().map((item) => ({
|
|
1920
|
+
hookId: item.hookId,
|
|
1921
|
+
hookPoints: item.registration.hookPoints.join(","),
|
|
1922
|
+
failPolicy: item.registration.failPolicy || "open",
|
|
1923
|
+
})))
|
|
1924
|
+
|
|
1925
|
+
console.table(sdk.getHookExecutionStats().map((item) => ({
|
|
1926
|
+
hookId: item.hookId,
|
|
1927
|
+
invocations: item.invocationCount,
|
|
1928
|
+
success: item.successCount,
|
|
1929
|
+
failure: item.failureCount,
|
|
1930
|
+
avgMs: Number(item.averageDurationMs.toFixed(2)),
|
|
1931
|
+
lastError: item.lastError || "",
|
|
1932
|
+
})))
|
|
1933
|
+
```
|
|
1934
|
+
|
|
1935
|
+
单点强类型注册辅助 API:
|
|
1936
|
+
|
|
1937
|
+
```typescript
|
|
1938
|
+
sdk.registerHookForPoint('before_tool', {
|
|
1939
|
+
matcher: { tool_names: ['run_bash_command'] },
|
|
1940
|
+
}, async (_ctx, payload) => {
|
|
1941
|
+
// payload 在 before_tool 下是强类型
|
|
1942
|
+
return { metadata: { tool: payload.tool_name } }
|
|
1943
|
+
})
|
|
1944
|
+
|
|
1945
|
+
await sdk.registerHookForPointAndWait('before_tool', {
|
|
1946
|
+
failPolicy: 'closed',
|
|
1947
|
+
}, async (_ctx, payload) => {
|
|
1948
|
+
return { decision: payload.tool_name === 'delete_file' ? 'ask' : 'passthrough' }
|
|
1949
|
+
})
|
|
1950
|
+
```
|
|
1951
|
+
|
|
1952
|
+
单点强类型 API 支持 `payloadValidation`:
|
|
1953
|
+
- `strict`(默认):payload 不匹配时本次 hook 调用失败。
|
|
1954
|
+
- `warn`:发出 SDK `error` 事件,但继续调用 handler。
|
|
1955
|
+
- `off`:关闭运行时 payload 校验。
|
|
1956
|
+
|
|
1957
|
+
Hook roundtrip 冒烟脚本:
|
|
1958
|
+
|
|
1959
|
+
```bash
|
|
1960
|
+
npm --prefix packages/sdk run test:hook-roundtrip-smoke
|
|
1961
|
+
```
|
|
1962
|
+
|
|
1963
|
+
该脚本会启动本地 WS mock backend,校验 13 个 hook 点的往返调用、strict 模式下 payload 不匹配失败路径、`unregisterHookAndWait` 的确定性成功/回滚路径,以及 `hookLifecycle` 事件。
|
|
1964
|
+
|
|
1965
|
+
### Hook 动作
|
|
1966
|
+
|
|
1967
|
+
Hook handler 返回 `null`(不干预)或 `HookResult`:
|
|
1968
|
+
|
|
1969
|
+
```typescript
|
|
1970
|
+
interface HookResult {
|
|
1971
|
+
skip?: boolean // 阻断操作(如阻止工具执行)
|
|
1972
|
+
value?: unknown // 替换值(如修改后的参数或响应)
|
|
1973
|
+
suspend?: boolean // 暂停 agent,等待人工确认
|
|
1974
|
+
suspend_payload?: unknown // 在 HITL 确认界面显示的数据
|
|
1975
|
+
metadata?: Record<string, unknown>
|
|
1976
|
+
decision?: 'deny' | 'ask' | 'passthrough' // 结构化决策
|
|
1977
|
+
reason?: string // deny/ask 的可读原因
|
|
1978
|
+
}
|
|
1979
|
+
```
|
|
1980
|
+
|
|
1981
|
+
后端聚合优先级:`deny > ask > passthrough`。
|
|
1982
|
+
|
|
1983
|
+
### Suspend(人机协作)
|
|
1984
|
+
|
|
1985
|
+
Hooks 可以暂停 agent 并等待外部确认:
|
|
1986
|
+
|
|
1987
|
+
```typescript
|
|
1988
|
+
sdk.registerHook({
|
|
1989
|
+
hookPoints: ['before_tool'],
|
|
1990
|
+
matcher: { tool_names: ['send_email'] },
|
|
1991
|
+
priority: 10,
|
|
1992
|
+
}, async (ctx, payload) => {
|
|
1993
|
+
// 人工确认后,hook 会带着 ctx.resume_value 重新触发
|
|
1994
|
+
if (ctx.resume_value) {
|
|
1995
|
+
return ctx.resume_value.approved ? null : { skip: true }
|
|
1996
|
+
}
|
|
1997
|
+
// 首次触发:挂起并显示确认界面
|
|
1998
|
+
return {
|
|
1999
|
+
suspend: true,
|
|
2000
|
+
suspend_payload: {
|
|
2001
|
+
question: `是否允许发送邮件给 ${payload.tool_args.to}?`,
|
|
2002
|
+
preview: JSON.stringify(payload.tool_args).substring(0, 500),
|
|
2003
|
+
},
|
|
2004
|
+
}
|
|
2005
|
+
})
|
|
2006
|
+
```
|
|
2007
|
+
|
|
2008
|
+
### 安全等级
|
|
2009
|
+
|
|
2010
|
+
Hook 点需要最低安全等级:
|
|
2011
|
+
|
|
2012
|
+
| 等级 | Hook 点 |
|
|
2013
|
+
|------|---------|
|
|
2014
|
+
| `standard` | on_user_prompt_submit, on_stop_failure, on_subagent_start, on_subagent_stop, on_run_start, on_run_end, on_loop, after_tool, before_compress |
|
|
2015
|
+
| `elevated` | before_tool, after_model, before_finalize |
|
|
2016
|
+
| `unrestricted` | before_model |
|
|
2017
|
+
|
|
2018
|
+
在 SDKConfig 中设置 `requestedSecurityLevel` 以匹配所需的 hook 点。
|
|
2019
|
+
|
|
2020
|
+
### 可靠性
|
|
2021
|
+
|
|
2022
|
+
- **失败策略**: `"open"`(默认)-- hook 超时/错误不阻断 agent。`"closed"` -- hook 失败时阻断操作(用于安全关键的 hook)。
|
|
2023
|
+
- **超时**: 按 hook 点差异化(tool: 10s, model: 30s, observation: 5s)。注册时可通过 `timeout` 覆盖。
|
|
2024
|
+
- **熔断器**: 连续 5 次失败后自动熔断(60s 恢复期)。
|
|
2025
|
+
|
|
2026
|
+
### Hook 可观测事件
|
|
2027
|
+
|
|
2028
|
+
```typescript
|
|
2029
|
+
sdk.on('hookInvoked', (event) => {
|
|
2030
|
+
console.log(event.hookPoint, event.callId, event.payload)
|
|
2031
|
+
})
|
|
2032
|
+
|
|
2033
|
+
sdk.on('hookResult', (event) => {
|
|
2034
|
+
console.log(event.hookPoint, event.success, event.durationMs, event.error)
|
|
2035
|
+
})
|
|
2036
|
+
|
|
2037
|
+
sdk.on('hookLifecycle', (event) => {
|
|
2038
|
+
console.log(
|
|
2039
|
+
event.action,
|
|
2040
|
+
event.hookId,
|
|
2041
|
+
event.success,
|
|
2042
|
+
event.deterministic,
|
|
2043
|
+
event.durationMs,
|
|
2044
|
+
event.error
|
|
2045
|
+
)
|
|
2046
|
+
})
|
|
2047
|
+
```
|
|
2048
|
+
|
|
2049
|
+
---
|
|
2050
|
+
|
|
1509
2051
|
## 能力发现 (Capability Discovery)
|
|
1510
2052
|
|
|
1511
2053
|
查询 Sanqian 的完整能力注册表。
|
|
@@ -1631,6 +2173,7 @@ const sdk = new SanqianSDK({
|
|
|
1631
2173
|
|
|
1632
2174
|
// 显示
|
|
1633
2175
|
displayName: 'My App', // Sanqian UI 中显示的名称
|
|
2176
|
+
requestedSecurityLevel: 'standard', // 可选:standard | elevated | unrestricted
|
|
1634
2177
|
|
|
1635
2178
|
// 启动
|
|
1636
2179
|
launchCommand: '/path/to/app', // Sanqian 启动你的应用的命令
|
|
@@ -1651,6 +2194,12 @@ const sdk = new SanqianSDK({
|
|
|
1651
2194
|
// 调试
|
|
1652
2195
|
debug: false, // 控制台日志(默认 false)
|
|
1653
2196
|
|
|
2197
|
+
// 注册握手(可选,默认值如下)
|
|
2198
|
+
protocolVersion: '2026-03-29', // 协议版本(YYYY-MM-DD)
|
|
2199
|
+
hookCapabilities: { // Hook 能力声明
|
|
2200
|
+
streamingHook: false, // 与后端协商(most-restrictive-wins)
|
|
2201
|
+
},
|
|
2202
|
+
|
|
1654
2203
|
// 浏览器模式
|
|
1655
2204
|
connectionInfo: undefined, // 预配置连接信息(跳过文件发现)
|
|
1656
2205
|
})
|
|
@@ -1745,6 +2294,8 @@ console.log(data.message.content)
|
|
|
1745
2294
|
|
|
1746
2295
|
## 内置工具参考
|
|
1747
2296
|
|
|
2297
|
+
下表是静态快照。运行时请以 `sdk.listTools('builtin')`(或 `/api/capabilities`)为准。
|
|
2298
|
+
|
|
1748
2299
|
### 文件操作
|
|
1749
2300
|
|
|
1750
2301
|
| 工具 | 说明 |
|
|
@@ -1834,6 +2385,12 @@ HTTP API 适合任意语言的简单对话。SDK 适合需要工具、Agent、
|
|
|
1834
2385
|
|
|
1835
2386
|
## Changelog
|
|
1836
2387
|
|
|
2388
|
+
### 0.3.28
|
|
2389
|
+
|
|
2390
|
+
- Remote hooks SDK API is now available in both Node and Browser builds (`registerHook` / `unregisterHook`, hook type exports, runtime hook invoke/result handling).
|
|
2391
|
+
- Added `requestedSecurityLevel` handshake support and hook declaration sync in registration payloads.
|
|
2392
|
+
- Hardened docs to match current runtime behavior and compatibility constraints.
|
|
2393
|
+
|
|
1837
2394
|
### 0.3.25
|
|
1838
2395
|
|
|
1839
2396
|
- Centralized HTTP request pipeline: all HTTP API calls now go through a unified `httpRequest()` method that automatically injects `X-App-Token` auth headers and standardizes error handling via `SanqianSDKError`. This prevents auth header omissions and ensures consistent error types for SDK consumers.
|