@hasna-internal/kai-user-questions 0.1.1-rc.2
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +46 -0
- package/README.zh.md +46 -0
- package/lib/index.js +76 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +65 -0
- package/lib/types/index.js +101 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +22 -0
- package/lib/types/types.d.ts +62 -0
- package/lib/types/types.js +8 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/interaction/user-questions/README.md
|
|
5
|
+
README.md: 53459c39c75d5d3907b002239c83db5f82e024f5
|
|
6
|
+
README.zh.md: 3a1f016aef6efa0a2167523fd4e1a938fa53eb71
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @hasna-internal/kai-user-questions
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
User-interaction Service Definition. It owns `ctx.userQuestions`, the service a model-facing tool or permission plugin uses when it needs to pause work and ask the human for a decision.
|
|
6
|
+
|
|
7
|
+
## Service: `UserQuestionService` (ctx key: `userQuestions`)
|
|
8
|
+
|
|
9
|
+
### Public API
|
|
10
|
+
|
|
11
|
+
- `ctx.userQuestions.registerProvider(provider): () => void` Register the UI-side provider. Only one provider may be active in a context; disposal unregisters it.
|
|
12
|
+
- `ctx.userQuestions.ask(request): Promise<AskUserQuestionAnswer>` Ask the active provider and wait for the answer.
|
|
13
|
+
|
|
14
|
+
### Key Types
|
|
15
|
+
|
|
16
|
+
- `AskUserQuestionRequest` — `{ questions: [{ id, question, detail?, header?, options?, multiSelect?, intent? }], agent?, signal? }`; `detail` supplies supporting text that providers render with the question without turning it into an option label. When present, `agent` must be the registry's exact live runtime root.
|
|
17
|
+
- `AskUserQuestionOption` — `{ label, description? }`.
|
|
18
|
+
- `AskUserQuestionIntent` — `{ kind: 'plan-review', approve }`; the tagged presentation intent below.
|
|
19
|
+
- `AskUserQuestionAnswer` — `{ answers: [{ id, selected, custom? }] }`.
|
|
20
|
+
- `UserQuestionProvider` — UI implementation with `ask(request)`.
|
|
21
|
+
- `UserQuestionError` — `HarnessError` subclass with codes such as `EMPTY_QUESTIONS`, `BAD_INTENT`, `NO_PROVIDER`, `DUPLICATE_PROVIDER`, `ASK_ABORTED`, `CALLER_NOT_LIVE`, and `DELEGATED_CALLER`.
|
|
22
|
+
|
|
23
|
+
For a single-select question, `custom` overrides the selected choice and `selected` is empty. For a multi-select question, `custom` may supplement the labels in `selected`. A UI may preserve a skipped item as `{ id, selected: [] }`, keeping the existing answer shape while retaining other answers in the batch.
|
|
24
|
+
|
|
25
|
+
When a request carries an agent, `ask()` authenticates its exact identity through the live `AgentRegistry` and admits only a runtime root. Durable lineage is not authority: a session with historical delegation depth may ask after it is resumed as a new runtime root, while a live child owned by another agent is rejected even if its durable depth is zero. Agentless programmatic requests retain the existing provider path.
|
|
26
|
+
|
|
27
|
+
### Presentation intent
|
|
28
|
+
|
|
29
|
+
`intent` declares that a question IS a known kind of decision, so a UI that recognises the tag may present it as such — `plan-review` says `detail` is a plan under review, and `dsh-plan-mode` sets it on the `exit_plan_mode` question. An intent changes presentation only: a UI honouring it answers with the same option labels a generic UI would send, and a UI that does not know the tag renders the generic option list, so callers read the same answer fields either way. `approve` names the label that approves rather than relying on option order. `ask()` rejects with `BAD_INTENT` the two assertions no type can carry: an `approve` naming none of that question's own options, and an intent on a question with no `detail` — the thing it declares itself a review of.
|
|
30
|
+
|
|
31
|
+
## Role
|
|
32
|
+
|
|
33
|
+
This is the Service Definition package. Consumers such as `@hasna-internal/kai-tool-ask-user` depend on this service; the Web host runtime supplies the shipped Service Provider. The loop stays unchanged: a tool call awaits a promise, and the tool result resumes the normal agent loop.
|
|
34
|
+
|
|
35
|
+
## Model Experience
|
|
36
|
+
|
|
37
|
+
Indirectly, through `dsh-tool-ask-user`, which retains a successful provider answer as compact JSON or one of these failures: `Error: ask_user_question was aborted before the user answered`, `Error: ask_user_question requires at least one question`, `Error: human interaction requires the exact live calling agent when an agent is supplied`, `Error: human interaction is unavailable while the calling agent is owned by another live agent; include the unresolved question or decision in the child agent's final result`, `Error: no user-questions provider is registered`, or `Error: <message>`. Waiting for the human adds no tokens.
|
|
38
|
+
|
|
39
|
+
#### KV Cache effect
|
|
40
|
+
|
|
41
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
42
|
+
|
|
43
|
+
## Known Limitations and Deferred Work
|
|
44
|
+
|
|
45
|
+
- **One provider per context** — there is no routing or fan-out to multiple UIs; a second registration throws `DUPLICATE_PROVIDER`, and with none registered `ask()` throws `NO_PROVIDER` rather than degrading.
|
|
46
|
+
- **The vocabulary is the question-form shape only** — selectable options plus optional custom text; richer interaction shapes (file pickers, diff-preview confirmations) have no seam vocabulary yet.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @hasna-internal/kai-user-questions
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
用户交互 Service Definition。它定义 `ctx.userQuestions`,供面向模型的工具或权限插件在需要暂停工作并询问人类决定时使用。
|
|
6
|
+
|
|
7
|
+
## 服务:`UserQuestionService`(ctx 键:`userQuestions`)
|
|
8
|
+
|
|
9
|
+
### 公开 API
|
|
10
|
+
|
|
11
|
+
- `ctx.userQuestions.registerProvider(provider): () => void` 注册 UI 侧提供方。同一上下文中只能有一个活跃提供方;dispose(资源释放)会将其注销。
|
|
12
|
+
- `ctx.userQuestions.ask(request): Promise<AskUserQuestionAnswer>` 向活跃提供方提问并等待回答。
|
|
13
|
+
|
|
14
|
+
### 关键类型
|
|
15
|
+
|
|
16
|
+
- `AskUserQuestionRequest`:`{ questions: [{ id, question, detail?, header?, options?, multiSelect?, intent? }], agent?, signal? }`;`detail` 提供辅助文本,提供方会将其随问题一起渲染,而不会将其变成选项标签。如提供 `agent`,它必须与注册表中的存活运行时根 agent(智能体)是同一对象。
|
|
17
|
+
- `AskUserQuestionOption`:`{ label, description? }`。
|
|
18
|
+
- `AskUserQuestionIntent`:`{ kind: 'plan-review', approve }`;即下文的带标签呈现意图。
|
|
19
|
+
- `AskUserQuestionAnswer`:`{ answers: [{ id, selected, custom? }] }`。
|
|
20
|
+
- `UserQuestionProvider`:包含 `ask(request)` 的 UI 实现。
|
|
21
|
+
- `UserQuestionError`:`HarnessError` 的子类,包含 `EMPTY_QUESTIONS`、`BAD_INTENT`、`NO_PROVIDER`、`DUPLICATE_PROVIDER`、`ASK_ABORTED`、`CALLER_NOT_LIVE` 和 `DELEGATED_CALLER` 等代码。
|
|
22
|
+
|
|
23
|
+
对于单选题,`custom` 会覆盖选中的选项,且 `selected` 为空。对于多选题,`custom` 可以补充 `selected` 中的标签。UI 可以把跳过的条目保留为 `{ id, selected: [] }`,既维持现有回答形态,也保留该批次中的其他回答。
|
|
24
|
+
|
|
25
|
+
请求包含 agent 时,`ask()` 会通过当前 `AgentRegistry` 验证该 agent 与注册表中的存活实例是同一对象,并且只允许运行时根调用。持久谱系不构成权限依据:带有历史委托深度的会话恢复为新的运行时根后可以提问;归属于另一个 agent 的存活子级即使持久化记录的委托深度为零也会被拒绝。不含 agent 的程序化请求继续沿用现有提供方路径。
|
|
26
|
+
|
|
27
|
+
### 呈现意图
|
|
28
|
+
|
|
29
|
+
`intent` 声明某个问题本身就是一种已知决策,因此认识该标签的 UI 可以照此呈现——`plan-review` 表示 `detail` 是一份待审阅的计划,`dsh-plan-mode` 会在 `exit_plan_mode` 的问题上设置它。意图只改变呈现:遵循它的 UI 回答的仍是通用 UI 会发送的那些选项标签,不认识该标签的 UI 渲染通用选项列表,因此调用方两种情况下读到的回答字段相同。`approve` 指名表示批准的标签,而不依赖选项顺序。有两项断言无法通过类型表达,`ask()` 会以 `BAD_INTENT` 拒绝它们:`approve` 未命中该问题自身的任一选项,以及意图落在没有 `detail` 的问题上——而 `detail` 正是它自称在审阅的东西。
|
|
30
|
+
|
|
31
|
+
## 职责
|
|
32
|
+
|
|
33
|
+
这是 Service Definition 包。`@hasna-internal/kai-tool-ask-user` 等 Consumer 依赖此服务;Web 宿主运行时提供随产品交付的 Service Provider。循环保持不变:工具调用等待 Promise,工具结果随后恢复正常的 agent loop(智能体循环)。
|
|
34
|
+
|
|
35
|
+
## 模型体验
|
|
36
|
+
|
|
37
|
+
间接地,通过 `dsh-tool-ask-user`:它会将成功的提供方回答保留为紧凑 JSON,或返回以下失败之一:`Error: ask_user_question was aborted before the user answered`、`Error: ask_user_question requires at least one question`、`Error: human interaction requires the exact live calling agent when an agent is supplied`、`Error: human interaction is unavailable while the calling agent is owned by another live agent; include the unresolved question or decision in the child agent's final result`、`Error: no user-questions provider is registered` 或 `Error: <message>`。等待人类回答不会增加 token。
|
|
38
|
+
|
|
39
|
+
#### KV Cache 影响
|
|
40
|
+
|
|
41
|
+
不会直接使 KV Cache 失效;请求前缀的任何变更均由上述消费方负责。
|
|
42
|
+
|
|
43
|
+
## 已知限制与暂缓事项
|
|
44
|
+
|
|
45
|
+
- **每个上下文只能有一个提供方**:不支持路由或扇出到多个 UI;第二次注册会抛出 `DUPLICATE_PROVIDER`,未注册任何提供方时,`ask()` 会抛出 `NO_PROVIDER`,而不会降级。
|
|
46
|
+
- **词汇仅包含问题表单形态**:可供选择的选项加可选的自定义文本;更丰富的交互形态(文件选择器、diff 预览确认)尚无 seam 词汇。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { HarnessError } from "@hasna-internal/kai-llm";
|
|
3
|
+
//#region lib/types/index.js
|
|
4
|
+
/**
|
|
5
|
+
* Service Definition for the user-questions capability seam (`ctx.userQuestions`): a UI-backed service for
|
|
6
|
+
* pausing an agent tool call until the human answers a question. The model-
|
|
7
|
+
* facing tool lives in `@hasna-internal/kai-tool-ask-user`; UI packages provide
|
|
8
|
+
* the single active provider.
|
|
9
|
+
*
|
|
10
|
+
* @module @hasna-internal/kai-user-questions
|
|
11
|
+
*/
|
|
12
|
+
/** Stable error taxonomy for user-questions failures. */
|
|
13
|
+
var UserQuestionError = class extends HarnessError {
|
|
14
|
+
constructor(message, code, options) {
|
|
15
|
+
super(message, code, options);
|
|
16
|
+
this.name = "UserQuestionError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** `ctx.userQuestions`: one active UI provider plus an `ask()` API. */
|
|
20
|
+
var UserQuestionService = class extends Service {
|
|
21
|
+
provider;
|
|
22
|
+
constructor(ctx) {
|
|
23
|
+
super(ctx, "userQuestions");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Register the UI provider. Only one provider may be active in a context.
|
|
27
|
+
*
|
|
28
|
+
* @param provider UI-side implementation that collects answers.
|
|
29
|
+
* @returns Disposer that unregisters this provider.
|
|
30
|
+
*/
|
|
31
|
+
registerProvider(provider) {
|
|
32
|
+
const dispose = this.ctx.effect(function* () {
|
|
33
|
+
if (this.provider !== void 0) throw new UserQuestionError("a user-questions provider is already registered", "DUPLICATE_PROVIDER");
|
|
34
|
+
this.provider = provider;
|
|
35
|
+
yield () => {
|
|
36
|
+
this.provider = void 0;
|
|
37
|
+
};
|
|
38
|
+
}.bind(this), "userInteraction.registerProvider()");
|
|
39
|
+
return () => void dispose();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Ask the active UI provider and wait for the user's answer.
|
|
43
|
+
*
|
|
44
|
+
* When a caller supplies an agent, human interaction is valid only for the
|
|
45
|
+
* exact live runtime root. Runtime ownership, not durable session lineage,
|
|
46
|
+
* decides this boundary: an owned child has no human answerer and would
|
|
47
|
+
* block forever, while a lineage-bearing session resumed as a new runtime
|
|
48
|
+
* root may ask normally.
|
|
49
|
+
*
|
|
50
|
+
* @param request Questions, owner agent, and abort signal.
|
|
51
|
+
* @returns The answer chosen or typed by the human.
|
|
52
|
+
* @throws {UserQuestionError} code `CALLER_NOT_LIVE` when a supplied
|
|
53
|
+
* agent is not the registry's exact live instance, or `DELEGATED_CALLER`
|
|
54
|
+
* when that live agent is owned by another agent.
|
|
55
|
+
*/
|
|
56
|
+
async ask(request) {
|
|
57
|
+
if (request.signal?.aborted) throw new UserQuestionError("ask_user_question was aborted before the user answered", "ASK_ABORTED");
|
|
58
|
+
if (request.questions.length === 0) throw new UserQuestionError("ask_user_question requires at least one question", "EMPTY_QUESTIONS");
|
|
59
|
+
const agent = request.agent;
|
|
60
|
+
if (agent !== void 0) {
|
|
61
|
+
const agents = this.ctx.get("agents");
|
|
62
|
+
if (agents === void 0 || agents.get(agent.id) !== agent) throw new UserQuestionError("human interaction requires the exact live calling agent when an agent is supplied", "CALLER_NOT_LIVE");
|
|
63
|
+
if (!agents.roots().includes(agent)) throw new UserQuestionError("human interaction is unavailable while the calling agent is owned by another live agent; include the unresolved question or decision in the child agent's final result", "DELEGATED_CALLER");
|
|
64
|
+
}
|
|
65
|
+
for (const question of request.questions) {
|
|
66
|
+
const intent = question.intent;
|
|
67
|
+
if (intent === void 0) continue;
|
|
68
|
+
if (!(question.options ?? []).some((option) => option.label === intent.approve)) throw new UserQuestionError(`question ${question.id} declares intent ${intent.kind} whose approve label ${JSON.stringify(intent.approve)} names none of its options`, "BAD_INTENT");
|
|
69
|
+
if (question.detail === void 0) throw new UserQuestionError(`question ${question.id} declares intent ${intent.kind} without the detail it reviews`, "BAD_INTENT");
|
|
70
|
+
}
|
|
71
|
+
if (this.provider === void 0) throw new UserQuestionError("no user-questions provider is registered", "NO_PROVIDER");
|
|
72
|
+
return this.provider.ask(request);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { UserQuestionError, UserQuestionService, UserQuestionService as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@hasna-internal/kai-user-questions`.
|
|
4
|
+
* @module @hasna-internal/kai-user-questions/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@hasna-internal/kai-user-questions";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "user-questions-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: the single provider slot is validated at registration and asks return
|
|
13
|
+
* directly to their caller; the seam publishes no independent request/answer audit stream.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the user-questions capability seam (`ctx.userQuestions`): a UI-backed service for
|
|
3
|
+
* pausing an agent tool call until the human answers a question. The model-
|
|
4
|
+
* facing tool lives in `@hasna-internal/kai-tool-ask-user`; UI packages provide
|
|
5
|
+
* the single active provider.
|
|
6
|
+
*
|
|
7
|
+
* @module @hasna-internal/kai-user-questions
|
|
8
|
+
*/
|
|
9
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
|
+
import type { Agent } from '@hasna-internal/kai-agent';
|
|
11
|
+
import { HarnessError } from '@hasna-internal/kai-llm';
|
|
12
|
+
declare module '@deepseek-ai/cordis' {
|
|
13
|
+
interface Context {
|
|
14
|
+
userQuestions: UserQuestionService;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
import type { AskUserQuestionAnswer, AskUserQuestionItem } from './types.ts';
|
|
18
|
+
export type { AskUserQuestionAnswer, AskUserQuestionAnswerItem, AskUserQuestionIntent, AskUserQuestionItem, AskUserQuestionOption, } from './types.ts';
|
|
19
|
+
/** Request for a human answer. */
|
|
20
|
+
export interface AskUserQuestionRequest {
|
|
21
|
+
/** Questions to display. */
|
|
22
|
+
questions: AskUserQuestionItem[];
|
|
23
|
+
/** Exact live calling agent, when the request came from an agent tool call. */
|
|
24
|
+
agent?: Agent;
|
|
25
|
+
/** Abort signal for the owning tool/step. */
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}
|
|
28
|
+
/** UI-side provider for user questions. */
|
|
29
|
+
export interface UserQuestionProvider {
|
|
30
|
+
ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>;
|
|
31
|
+
}
|
|
32
|
+
/** Stable error taxonomy for user-questions failures. */
|
|
33
|
+
export declare class UserQuestionError extends HarnessError {
|
|
34
|
+
constructor(message: string, code: string, options?: ErrorOptions);
|
|
35
|
+
}
|
|
36
|
+
/** `ctx.userQuestions`: one active UI provider plus an `ask()` API. */
|
|
37
|
+
export declare class UserQuestionService extends Service {
|
|
38
|
+
private provider;
|
|
39
|
+
constructor(ctx: Context);
|
|
40
|
+
/**
|
|
41
|
+
* Register the UI provider. Only one provider may be active in a context.
|
|
42
|
+
*
|
|
43
|
+
* @param provider UI-side implementation that collects answers.
|
|
44
|
+
* @returns Disposer that unregisters this provider.
|
|
45
|
+
*/
|
|
46
|
+
registerProvider(provider: UserQuestionProvider): () => void;
|
|
47
|
+
/**
|
|
48
|
+
* Ask the active UI provider and wait for the user's answer.
|
|
49
|
+
*
|
|
50
|
+
* When a caller supplies an agent, human interaction is valid only for the
|
|
51
|
+
* exact live runtime root. Runtime ownership, not durable session lineage,
|
|
52
|
+
* decides this boundary: an owned child has no human answerer and would
|
|
53
|
+
* block forever, while a lineage-bearing session resumed as a new runtime
|
|
54
|
+
* root may ask normally.
|
|
55
|
+
*
|
|
56
|
+
* @param request Questions, owner agent, and abort signal.
|
|
57
|
+
* @returns The answer chosen or typed by the human.
|
|
58
|
+
* @throws {UserQuestionError} code `CALLER_NOT_LIVE` when a supplied
|
|
59
|
+
* agent is not the registry's exact live instance, or `DELEGATED_CALLER`
|
|
60
|
+
* when that live agent is owned by another agent.
|
|
61
|
+
*/
|
|
62
|
+
ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>;
|
|
63
|
+
}
|
|
64
|
+
export default UserQuestionService;
|
|
65
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the user-questions capability seam (`ctx.userQuestions`): a UI-backed service for
|
|
3
|
+
* pausing an agent tool call until the human answers a question. The model-
|
|
4
|
+
* facing tool lives in `@hasna-internal/kai-tool-ask-user`; UI packages provide
|
|
5
|
+
* the single active provider.
|
|
6
|
+
*
|
|
7
|
+
* @module @hasna-internal/kai-user-questions
|
|
8
|
+
*/
|
|
9
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
10
|
+
import { HarnessError } from '@hasna-internal/kai-llm';
|
|
11
|
+
/** Stable error taxonomy for user-questions failures. */
|
|
12
|
+
export class UserQuestionError extends HarnessError {
|
|
13
|
+
constructor(message, code, options) {
|
|
14
|
+
super(message, code, options);
|
|
15
|
+
this.name = 'UserQuestionError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** `ctx.userQuestions`: one active UI provider plus an `ask()` API. */
|
|
19
|
+
export class UserQuestionService extends Service {
|
|
20
|
+
provider;
|
|
21
|
+
constructor(ctx) {
|
|
22
|
+
super(ctx, 'userQuestions');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register the UI provider. Only one provider may be active in a context.
|
|
26
|
+
*
|
|
27
|
+
* @param provider UI-side implementation that collects answers.
|
|
28
|
+
* @returns Disposer that unregisters this provider.
|
|
29
|
+
*/
|
|
30
|
+
registerProvider(provider) {
|
|
31
|
+
const dispose = this.ctx.effect(function* () {
|
|
32
|
+
if (this.provider !== undefined) {
|
|
33
|
+
throw new UserQuestionError('a user-questions provider is already registered', 'DUPLICATE_PROVIDER');
|
|
34
|
+
}
|
|
35
|
+
this.provider = provider;
|
|
36
|
+
yield () => {
|
|
37
|
+
this.provider = undefined;
|
|
38
|
+
};
|
|
39
|
+
}.bind(this), 'userInteraction.registerProvider()');
|
|
40
|
+
return () => void dispose();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Ask the active UI provider and wait for the user's answer.
|
|
44
|
+
*
|
|
45
|
+
* When a caller supplies an agent, human interaction is valid only for the
|
|
46
|
+
* exact live runtime root. Runtime ownership, not durable session lineage,
|
|
47
|
+
* decides this boundary: an owned child has no human answerer and would
|
|
48
|
+
* block forever, while a lineage-bearing session resumed as a new runtime
|
|
49
|
+
* root may ask normally.
|
|
50
|
+
*
|
|
51
|
+
* @param request Questions, owner agent, and abort signal.
|
|
52
|
+
* @returns The answer chosen or typed by the human.
|
|
53
|
+
* @throws {UserQuestionError} code `CALLER_NOT_LIVE` when a supplied
|
|
54
|
+
* agent is not the registry's exact live instance, or `DELEGATED_CALLER`
|
|
55
|
+
* when that live agent is owned by another agent.
|
|
56
|
+
*/
|
|
57
|
+
async ask(request) {
|
|
58
|
+
if (request.signal?.aborted) {
|
|
59
|
+
throw new UserQuestionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED');
|
|
60
|
+
}
|
|
61
|
+
if (request.questions.length === 0) {
|
|
62
|
+
throw new UserQuestionError('ask_user_question requires at least one question', 'EMPTY_QUESTIONS');
|
|
63
|
+
}
|
|
64
|
+
const agent = request.agent;
|
|
65
|
+
if (agent !== undefined) {
|
|
66
|
+
const agents = this.ctx.get('agents');
|
|
67
|
+
if (agents === undefined || agents.get(agent.id) !== agent) {
|
|
68
|
+
throw new UserQuestionError('human interaction requires the exact live calling agent when an agent is supplied', 'CALLER_NOT_LIVE');
|
|
69
|
+
}
|
|
70
|
+
if (!agents.roots().includes(agent)) {
|
|
71
|
+
throw new UserQuestionError('human interaction is unavailable while the calling agent is owned by another live agent; '
|
|
72
|
+
+ "include the unresolved question or decision in the child agent's final result", 'DELEGATED_CALLER');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// A presentation intent asserts two things the types cannot: that the
|
|
76
|
+
// named approve label is one of this question's own options, and that a
|
|
77
|
+
// plan-review carries the plan it is a review of. A UI honouring the
|
|
78
|
+
// intent answers with that label, and shows that detail as the plan, so
|
|
79
|
+
// either gap would put a choice the asker never offered — or an approval of
|
|
80
|
+
// something invisible — in front of the user. Caught at the asker, where
|
|
81
|
+
// the mistake is, rather than in each UI.
|
|
82
|
+
for (const question of request.questions) {
|
|
83
|
+
const intent = question.intent;
|
|
84
|
+
if (intent === undefined)
|
|
85
|
+
continue;
|
|
86
|
+
if (!(question.options ?? []).some(option => option.label === intent.approve)) {
|
|
87
|
+
throw new UserQuestionError(`question ${question.id} declares intent ${intent.kind} whose approve label `
|
|
88
|
+
+ `${JSON.stringify(intent.approve)} names none of its options`, 'BAD_INTENT');
|
|
89
|
+
}
|
|
90
|
+
if (question.detail === undefined) {
|
|
91
|
+
throw new UserQuestionError(`question ${question.id} declares intent ${intent.kind} without the detail it reviews`, 'BAD_INTENT');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (this.provider === undefined) {
|
|
95
|
+
throw new UserQuestionError('no user-questions provider is registered', 'NO_PROVIDER');
|
|
96
|
+
}
|
|
97
|
+
return this.provider.ask(request);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export default UserQuestionService;
|
|
101
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@hasna-internal/kai-user-questions`.
|
|
3
|
+
* @module @hasna-internal/kai-user-questions/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "user-questions-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@hasna-internal/kai-user-questions`.
|
|
3
|
+
* @module @hasna-internal/kai-user-questions/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@hasna-internal/kai-user-questions';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'user-questions-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: the single provider slot is validated at registration and asks return
|
|
12
|
+
* directly to their caller; the seam publishes no independent request/answer audit stream.
|
|
13
|
+
*/
|
|
14
|
+
const install = () => { };
|
|
15
|
+
/**
|
|
16
|
+
* Register this package's invariant companion.
|
|
17
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
18
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
19
|
+
*/
|
|
20
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
21
|
+
/* jscpd:ignore-end */
|
|
22
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-safe question and answer types, free of cordis/service imports so browser
|
|
3
|
+
* type chains (apiproxy api → client) can consume them without loading this
|
|
4
|
+
* package's Context augmentation.
|
|
5
|
+
* @module @hasna-internal/kai-user-questions/types
|
|
6
|
+
*/
|
|
7
|
+
/** One selectable answer offered to the user. */
|
|
8
|
+
export interface AskUserQuestionOption {
|
|
9
|
+
/** User-facing label. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Optional extra context rendered by capable UIs. */
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A caller-declared presentation intent: the question IS this kind of
|
|
16
|
+
* decision, so a UI that recognises the tag may present it as such instead of as a
|
|
17
|
+
* generic option list. Tagged so further intents can be added; a UI that does
|
|
18
|
+
* not know a tag renders the generic flow, and the answer encoding is identical
|
|
19
|
+
* either way — an intent changes presentation only, never the protocol.
|
|
20
|
+
*/
|
|
21
|
+
export type AskUserQuestionIntent = {
|
|
22
|
+
/** A plan submitted for review: `detail` is the plan markdown `ask()` requires, and the decision approves or declines it. */
|
|
23
|
+
kind: 'plan-review';
|
|
24
|
+
/**
|
|
25
|
+
* The option label that approves the plan; every other option declines it.
|
|
26
|
+
* Named rather than positional so no UI infers the verdict from option order.
|
|
27
|
+
* An `approve` naming no option of its own question is rejected at `ask()`.
|
|
28
|
+
*/
|
|
29
|
+
approve: string;
|
|
30
|
+
};
|
|
31
|
+
/** One question in a user-questions request. */
|
|
32
|
+
export interface AskUserQuestionItem {
|
|
33
|
+
/** Stable caller-provided question id, echoed in the answer. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** The question to display. */
|
|
36
|
+
question: string;
|
|
37
|
+
/** Optional supporting detail rendered with the question but kept out of option labels. */
|
|
38
|
+
detail?: string;
|
|
39
|
+
/** Optional short heading/group label. */
|
|
40
|
+
header?: string;
|
|
41
|
+
/** Optional choices the UI can render as a menu. */
|
|
42
|
+
options?: AskUserQuestionOption[];
|
|
43
|
+
/** Whether more than one option may be selected. Defaults to single-select. */
|
|
44
|
+
multiSelect?: boolean;
|
|
45
|
+
/** Optional presentation intent for capable UIs; absent asks for the generic option list. */
|
|
46
|
+
intent?: AskUserQuestionIntent;
|
|
47
|
+
}
|
|
48
|
+
/** Answer to one question. */
|
|
49
|
+
export interface AskUserQuestionAnswerItem {
|
|
50
|
+
/** The answered question id. */
|
|
51
|
+
id: string;
|
|
52
|
+
/** Selected option labels. May accompany custom text for a multi-select question. */
|
|
53
|
+
selected: string[];
|
|
54
|
+
/** Optional free-text "Other" answer. */
|
|
55
|
+
custom?: string;
|
|
56
|
+
}
|
|
57
|
+
/** The human's answer. */
|
|
58
|
+
export interface AskUserQuestionAnswer {
|
|
59
|
+
/** Structured answers keyed by question id. */
|
|
60
|
+
answers: AskUserQuestionAnswerItem[];
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-safe question and answer types, free of cordis/service imports so browser
|
|
3
|
+
* type chains (apiproxy api → client) can consume them without loading this
|
|
4
|
+
* package's Context augmentation.
|
|
5
|
+
* @module @hasna-internal/kai-user-questions/types
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasna-internal/kai-user-questions",
|
|
3
|
+
"description": "Abstract user-questions seam (ctx.userQuestions) for asking the human during agent runs",
|
|
4
|
+
"version": "0.1.1-rc.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/interaction/user-questions"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./types": {
|
|
26
|
+
"types": "./lib/types/types.d.ts",
|
|
27
|
+
"default": "./lib/types/types.js"
|
|
28
|
+
},
|
|
29
|
+
"./src/*": "./src/*",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib/index.js",
|
|
34
|
+
"lib/invariant.js",
|
|
35
|
+
"lib/types/**/*.js",
|
|
36
|
+
"lib/types/**/*.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@hasna-internal/kai-agent": "^0.1.1-rc.2",
|
|
41
|
+
"@hasna-internal/kai-invariants": "^0.1.1-rc.2",
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
43
|
+
"@hasna-internal/kai-llm": "^0.1.1-rc.2"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@hasna-internal/kai-agent": "^0.1.1-rc.2",
|
|
47
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
|
+
"@hasna-internal/kai-invariants": "^0.1.1-rc.2",
|
|
49
|
+
"@hasna-internal/kai-llm": "^0.1.1-rc.2"
|
|
50
|
+
}
|
|
51
|
+
}
|