@yushaw/sanqian-sdk 0.3.28 → 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.
@@ -360,6 +371,10 @@ const response = await sdk.chat('agent', messages, {
360
371
 
361
372
  `uploadFile(localPath)` is not a remote file transfer protocol. For remote/backend-on-another-machine cases, continue using explicit byte upload APIs.
362
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
+
363
378
  ---
364
379
 
365
380
  ## Human-in-the-Loop (HITL)
@@ -600,6 +615,239 @@ sdk.on('resourceRemoved', (resourceId) => {
600
615
 
601
616
  ---
602
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
+
603
851
  ## Capability Discovery
604
852
 
605
853
  Query Sanqian's full capability registry: tools, skills, agents, and context providers.
@@ -774,6 +1022,7 @@ const sdk = new SanqianSDK({
774
1022
 
775
1023
  // Display
776
1024
  displayName: 'My App', // Shown in Sanqian UI
1025
+ requestedSecurityLevel: 'standard', // Optional: standard | elevated | unrestricted
777
1026
 
778
1027
  // Launch
779
1028
  launchCommand: '/path/to/app', // For Sanqian to start your app
@@ -796,6 +1045,12 @@ const sdk = new SanqianSDK({
796
1045
  // Debug
797
1046
  debug: false, // Console logging (default: false)
798
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
+
799
1054
  // Browser mode (see Browser Build section)
800
1055
  connectionInfo: undefined, // Pre-configured connection (skips file discovery)
801
1056
  })
@@ -944,6 +1199,7 @@ console.log(data.message.content)
944
1199
  ## Built-in Tools Reference
945
1200
 
946
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.
947
1203
 
948
1204
  ### File Operations
949
1205
 
@@ -1048,6 +1304,8 @@ Sanqian 提供两种外部集成方式:
1048
1304
  | **HTTP API** | 简单对话,任意语言 | REST + SSE |
1049
1305
  | **SDK** | 工具注册、Agent、上下文注入、深度集成 | WebSocket |
1050
1306
 
1307
+ > 文档基线:`@yushaw/sanqian-sdk@0.3.28`
1308
+
1051
1309
  ## 快速开始
1052
1310
 
1053
1311
  ### HTTP API
@@ -1093,6 +1351,15 @@ const sdk = new SanqianSDK({
1093
1351
 
1094
1352
  ---
1095
1353
 
1354
+ ## 推荐阅读路径
1355
+
1356
+ - 第一次做工具集成:`工具 (Tools)` -> `连接` -> `浏览器构建`(如需要)
1357
+ - 自定义 Agent 工作流:`Agent` -> `对话 (Chat)` -> `会话管理`
1358
+ - 安全干预/护栏:`Hooks` -> `人机协作 (HITL)` -> `安全等级`
1359
+ - 渠道机器人接入:`消息渠道 API` -> `能力发现`
1360
+
1361
+ ---
1362
+
1096
1363
  ## 工具 (Tools)
1097
1364
 
1098
1365
  工具是你的应用向 Sanqian 提供能力的主要方式。当用户提问时,AI Agent 可以调用你的工具来获取数据或执行操作。
@@ -1366,6 +1633,10 @@ const response = await sdk.chat('agent', messages, {
1366
1633
 
1367
1634
  `uploadFile(localPath)` 不是远程文件传输协议。对于远程 backend 或不同机器场景,仍应使用显式字节上传能力。
1368
1635
 
1636
+ 会话目标契约:
1637
+ - 未传 `conversationId` 时,后端可能创建新的 SDK conversation,并通过 `uploaded.conversationId` 返回。
1638
+ - 传了 `conversationId` 但该会话不存在时,`uploadFile()` 会抛出 `SanqianSDKError`,错误码为 `CONVERSATION_NOT_FOUND`。
1639
+
1369
1640
  ---
1370
1641
 
1371
1642
  ## 人机协作 (HITL)
@@ -1544,6 +1815,239 @@ for await (const event of sdk.chatStream('agent', messages, {
1544
1815
 
1545
1816
  ---
1546
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
+
1547
2051
  ## 能力发现 (Capability Discovery)
1548
2052
 
1549
2053
  查询 Sanqian 的完整能力注册表。
@@ -1669,6 +2173,7 @@ const sdk = new SanqianSDK({
1669
2173
 
1670
2174
  // 显示
1671
2175
  displayName: 'My App', // Sanqian UI 中显示的名称
2176
+ requestedSecurityLevel: 'standard', // 可选:standard | elevated | unrestricted
1672
2177
 
1673
2178
  // 启动
1674
2179
  launchCommand: '/path/to/app', // Sanqian 启动你的应用的命令
@@ -1689,6 +2194,12 @@ const sdk = new SanqianSDK({
1689
2194
  // 调试
1690
2195
  debug: false, // 控制台日志(默认 false)
1691
2196
 
2197
+ // 注册握手(可选,默认值如下)
2198
+ protocolVersion: '2026-03-29', // 协议版本(YYYY-MM-DD)
2199
+ hookCapabilities: { // Hook 能力声明
2200
+ streamingHook: false, // 与后端协商(most-restrictive-wins)
2201
+ },
2202
+
1692
2203
  // 浏览器模式
1693
2204
  connectionInfo: undefined, // 预配置连接信息(跳过文件发现)
1694
2205
  })
@@ -1783,6 +2294,8 @@ console.log(data.message.content)
1783
2294
 
1784
2295
  ## 内置工具参考
1785
2296
 
2297
+ 下表是静态快照。运行时请以 `sdk.listTools('builtin')`(或 `/api/capabilities`)为准。
2298
+
1786
2299
  ### 文件操作
1787
2300
 
1788
2301
  | 工具 | 说明 |
@@ -1872,6 +2385,12 @@ HTTP API 适合任意语言的简单对话。SDK 适合需要工具、Agent、
1872
2385
 
1873
2386
  ## Changelog
1874
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
+
1875
2394
  ### 0.3.25
1876
2395
 
1877
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.