@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 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.