@x-otto/interchange 0.1.0-alpha.5 → 0.1.0-alpha.6
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/dist/context-fragment.d.ts +37 -0
- package/dist/context-fragment.js +2 -0
- package/dist/image.d.ts +39 -0
- package/dist/image.js +1 -0
- package/dist/index.d.ts +250 -505
- package/dist/index.js +2 -3
- package/dist/input.d.ts +230 -0
- package/dist/input.js +1 -0
- package/dist/message.d.ts +216 -0
- package/dist/message.js +1 -0
- package/package.json +17 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
`)}const l=/^data:([^;,]*)(?:;[^,]*)?,(.*)$/s;function u(e,t){let n=l.exec(e),r=(n?n[2]??``:e).replace(/\s+/g,``);return{base64:r,mediaType:((n?n[1]:``)||t||``).toLowerCase(),valid:d(r)}}function d(e){return e.length===0||e===`undefined`||e.length%4==1?!1:/^[A-Za-z0-9+/]*={0,2}$/.test(e)}function f(e,t){switch(e.toLowerCase()){case`image/jpeg`:case`image/jpg`:return`image/jpeg`;case`image/png`:return`image/png`;case`image/gif`:return`image/gif`;case`image/webp`:return`image/webp`;default:return p(t)}}function p(e){return e.startsWith(`/9j/`)?`image/jpeg`:e.startsWith(`iVBOR`)?`image/png`:e.startsWith(`R0lGOD`)?`image/gif`:e.startsWith(`UklGR`)?`image/webp`:`image/png`}function m(e){return e===`hashtag`?`#`:`@`}const h=/(?:^|\s)([#@])(\S*)$/;function g(e,t){let n=e.slice(0,t).match(h);if(!n)return null;let r=n[1],i=n[2]??``;return{sigil:r,query:i,anchor:t-i.length-1}}function _(e,t){return!!t&&t.sigil===e.sigil&&t.anchor===e.anchor}function v(e,t){let n=Math.min(e.length,t.length),r=0;for(;r<n&&e[r]===t[r];)r++;let i=n-r,a=0;for(;a<i&&e[e.length-1-a]===t[t.length-1-a];)a++;return{start:r,end:e.length-a,delta:t.length-e.length}}function y(e,t,n){if(!e||t===n)return e;let{start:r,end:i,delta:a}=v(t,n),o=e.anchor+e.tokenLength;return i<=e.anchor?{...e,anchor:e.anchor+a}:r>=o||r>e.anchor&&r<o?e:null}function b(e,t,n,r){if(n<0||n>t)return{text:e,offset:t};let i=t>=e.length?` `:``;return{text:e.slice(0,n)+r.value+i+e.slice(t),offset:n+r.value.length+(i?1:0)}}function x(e,t,n,r){return t<0||t>n||n>e.length?{text:e,offset:n}:{text:e.slice(0,t)+r.value+e.slice(n),offset:t+r.value.length}}function S(e){let{source:t,oldText:n,newText:r,cursorOffset:i}=e,a=e.sigilFilter??(e=>e===`@`),o=y(e.dismissed,n,r);if(t!==`typing`||r.length<=n.length)return{action:`suppress`,dismissed:o};let s=g(r,i),c=s&&a(s.sigil)?s:null;return c?_(c,o)?{action:`suppress`,dismissed:o}:{action:`open`,trigger:c,dismissed:null}:{action:`close`,dismissed:o}}export{e as CONVERSATION_SUMMARY_PREFIX,y as adjustDismissedSigil,b as applySigilCompletion,x as applySigilReplacement,s as boundFragment,a as collectModelCapabilityFacts,o as contextFragmentBytes,S as evaluateSigilTrigger,g as extractSigilPrefix,c as extractToolResultText,t as getToolCallsByAssistantMessage,n as hasToolCalls,_ as isSigilDismissed,r as modelImageCapability,i as modelSupportsImages,u as normalizeImageData,m as sigilOf,f as toAnthropicImageMime};
|
|
1
|
+
import{CONVERSATION_SUMMARY_PREFIX as e,getToolCallsByAssistantMessage as t,hasToolCalls as n}from"./message.js";import{boundFragment as r,contextFragmentBytes as i}from"./context-fragment.js";import{normalizeImageData as a,toAnthropicImageMime as o}from"./image.js";import{adjustDismissedSigil as s,applySigilCompletion as c,applySigilReplacement as l,evaluateSigilTrigger as u,extractSigilPrefix as d,isSigilDismissed as f,sigilOf as p}from"./input.js";function m(e){return!Array.isArray(e.input)||e.input.length===0?{state:`unknown`}:e.input.includes(`image`)?{state:`supported`,value:!0}:{state:`unsupported`}}function h(e){return m(e).state===`supported`}function g(e){return{vision:m(e)}}const _={toolGuidance:{name:`tool-guidance-injection`,priority:20,lane:`stable`,budgetBytes:2e4},skillLoopGuidance:{name:`skill-loop-guidance-injection`,priority:21,lane:`stable`,budgetBytes:2e3},environment:{name:`runtime:environment`,priority:25,lane:`stable`,budgetBytes:8e3},agentKnowledge:{name:`agent-knowledge-injection`,priority:35,lane:`stable`,budgetBytes:28e3},skillCatalog:{name:`skill-catalog-injection`,priority:38,lane:`stable`,budgetBytes:12e3},lessonRelevantHints:{name:`lesson-relevant-hints`,priority:41,lane:`volatile`,budgetBytes:4e3},internalize:{name:`internalize-skill-injection`,priority:42,lane:`volatile`,budgetBytes:4e3},memoryDelta:{name:`memory-delta-injection`,priority:60,lane:`volatile`,budgetBytes:8e3},todoReminder:{name:`todo-reminder-injection`,priority:62,lane:`volatile`,budgetBytes:4e3},docSyncReminder:{name:`doc-sync-reminder-injection`,priority:64,lane:`volatile`,budgetBytes:4e3}},v=Object.values(_),y=new Set;function b(e,t){let n=v.find(t=>t.name===e);return n?(Buffer.byteLength(t,`utf8`)>n.budgetBytes&&!y.has(e)&&(y.add(e),console.warn(`[system-prompt] section "${e}" truncated to ${n.budgetBytes} bytes (content ${Buffer.byteLength(t,`utf8`)} bytes) — check SYSTEM_PROMPT_SECTIONS budget vs content size`)),r({id:x(e),content:t},n.budgetBytes).content):t}function x(e){let t={"tool-guidance-injection":`toolGuidance`,"skill-loop-guidance-injection":`skillLoopGuidance`,"runtime:environment":`environment`,"agent-knowledge-injection":`agentKnowledge`,"runtime:memory-index":`memoryIndex`,"skill-catalog-injection":`skillCatalog`,"lesson-injection":`lesson`,"lesson-relevant-hints":`lessonRelevantHints`,"internalize-skill-injection":`internalize`,"memory-delta-injection":`memoryDelta`,"todo-reminder-injection":`todoReminder`,"doc-sync-reminder-injection":`docSyncReminder`}[e];if(t===void 0)throw Error(`applySectionBudget: unknown section name "${e}"`);return t}function S(e){return e.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
|
|
2
|
+
`)}const C={"session.start":`durable`,"session.end":`durable`,"session.cleared":`durable`,"prompt.start":`durable`,"prompt.end":`durable`,"prompt.continued":`durable`,"turn.start":`durable`,"turn.end":`durable`,"tool.call.start":`durable`,"tool.call.end":`durable`,"messages.persist":`durable`,"compaction.start":`durable`,"compaction.end":`durable`,"memory.pruned":`durable`,error:`durable`,"stream.event":`live`,"streaming.start":`live`,"streaming.end":`live`,"agent.status":`live`,"steer.consumed":`live`,"run.paused":`live`,"run.resumed":`live`,"run.pause_requested":`live`,"run.pause_cancelled":`live`,"approval.required":`capability`,"approval.resolved":`capability`,"ask.user.required":`capability`,"ask.user.resolved":`capability`,"plan.approval.required":`capability`,"plan.approval.resolved":`capability`,"budget.notice":`capability`,"prompt.budget.notice":`capability`,"tool.call.mismatch.notice":`capability`,"image.degradation.notice":`capability`,"progress.stall.notice":`capability`,"compaction.reactive.notice":`capability`,"session.cost-budget-exceeded":`capability`,"session.crash-gap-detected":`capability`,"session.history-capped":`capability`,"session.write-lease-denied":`capability`,"session.write-lease-restored":`capability`};function w(e){return C[e]}export{e as CONVERSATION_SUMMARY_PREFIX,C as EVENT_DOMAIN,_ as SYSTEM_PROMPT_SECTIONS,v as SYSTEM_PROMPT_SECTION_ORDER,s as adjustDismissedSigil,b as applySectionBudget,c as applySigilCompletion,l as applySigilReplacement,r as boundFragment,g as collectModelCapabilityFacts,i as contextFragmentBytes,u as evaluateSigilTrigger,d as extractSigilPrefix,S as extractToolResultText,w as getEventDomain,t as getToolCallsByAssistantMessage,n as hasToolCalls,f as isSigilDismissed,m as modelImageCapability,h as modelSupportsImages,a as normalizeImageData,p as sigilOf,o as toAnthropicImageMime};
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
//#region src/input.d.ts
|
|
2
|
+
/** Input: mentions, sigils, content providers — user input enrichment types. */
|
|
3
|
+
/** sigil 子类型:`hashtag` 属 `#`,其余属 `@`。`resource` = 插件自定义资源(RFC-210 D2);
|
|
4
|
+
* `shortcut` = 用户在本仓库自定义的文本快捷方式(RFC-233);`effort` = 推理投入档
|
|
5
|
+
* (low/medium/high/xhigh/max,RFC-360 D3——不复用 `model`,两者在 `@` 面板需独立
|
|
6
|
+
* 分类展示,复用会导致 effort 档位被误标"模型"分类)。 */
|
|
7
|
+
type SigilKind = 'hashtag' | 'file' | 'person' | 'agent' | 'skill' | 'mcp' | 'plugin' | 'model' | 'resource' | 'shortcut' | 'effort';
|
|
8
|
+
/** 触发前缀。 */
|
|
9
|
+
type SigilPrefix = '#' | '@';
|
|
10
|
+
/** 条目来源标识(unregisterBySource 据此批量注销,防僵尸条目)。 */
|
|
11
|
+
type SigilSource = 'builtin' | 'plugin' | 'skill' | 'mcp' | (string & {});
|
|
12
|
+
/**
|
|
13
|
+
* 动态供给函数(pull 模型):按 query 懒查候选,服务 @file 等大候选集——
|
|
14
|
+
* 不全量 register()。可同步或异步(文件系统遍历走异步)。
|
|
15
|
+
*/
|
|
16
|
+
type SigilProvider = (query: string, sigil: SigilPrefix) => SigilEntry[] | Promise<SigilEntry[]>;
|
|
17
|
+
/** sigil 面板条目:label(展示)与 value(替换内容)严格分离(R2)。 */
|
|
18
|
+
interface SigilEntry {
|
|
19
|
+
/** 面板中展示的名称(简短、人类可读)。绝不进入 prompt 文本。 */
|
|
20
|
+
label: string;
|
|
21
|
+
/** 选中后替换 #trigger/@trigger 的实质内容。绝不在面板中完整展示。 */
|
|
22
|
+
value: string;
|
|
23
|
+
/** 面板中展示的描述(可选,列表副标题)。 */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** 条目类型(影响面板分组/着色,并决定属于 # 还是 @)。 */
|
|
26
|
+
kind: SigilKind;
|
|
27
|
+
/** 来源标识(builtin | plugin | skill | mcp | 自定义 pluginId)。 */
|
|
28
|
+
source?: SigilSource;
|
|
29
|
+
/**
|
|
30
|
+
* RFC-210 D2:resolve 引用。非空 = "提交时展开"型资源——展开管线(app.expandSigilChips)
|
|
31
|
+
* 用此 id 查 SigilResolverRegistry 拿 resolve 函数,把该条目的 chip 替换为其返回值。
|
|
32
|
+
* 空 = 透传型(chip 原样给 LLM)或纯文本型(value 即内容),行为与现状完全一致。
|
|
33
|
+
*/
|
|
34
|
+
resolverId?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* RFC-210 D2:@ 资源提交时展开函数(代码契约——不可序列化,归 coding 层
|
|
38
|
+
* SigilResolverRegistry,与纯数据的 PluginInputRegistry 平行;D9 两类契约纪律)。
|
|
39
|
+
*/
|
|
40
|
+
interface SigilResolver {
|
|
41
|
+
/** resolver id,命名空间 `<pluginId>:<name>`(内置无前缀)。 */
|
|
42
|
+
id: string;
|
|
43
|
+
/** 提交时展开:输入 chip 参数(`[@kind:param]` 的 param 段),返回发给 LLM 的内容。 */
|
|
44
|
+
resolve: (param: string, ctx: SigilResolveContext) => string | Promise<string>;
|
|
45
|
+
/** 超时(ms,缺省 10_000,对齐既有 CONTEXT_CHIPS 约束)。 */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
/**
|
|
48
|
+
* uiHostOnly(RFC-210 评审 B11):true = 读取"UI 所在机器"的本地资源(非 workspaceDir
|
|
49
|
+
* 内容),只应在与用户同机的宿主进程(TUI/cli)执行;service expand 端点跳过执行。
|
|
50
|
+
* @clipboard = true(读桌面剪贴板);@changes/@git = false(读 workspaceDir git 状态,
|
|
51
|
+
* service 为远程 workspace 服务时服务器上的状态正是用户要的内容)。
|
|
52
|
+
*/
|
|
53
|
+
uiHostOnly?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** SigilResolver.resolve 的执行上下文。 */
|
|
56
|
+
interface SigilResolveContext {
|
|
57
|
+
workspaceDir: string;
|
|
58
|
+
/** 输出上限(字符,缺省 20_000,超限截断——对齐既有 MAX_OUTPUT)。 */
|
|
59
|
+
maxOutput: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 插件输入注册表(UI 无关,由 createApp() 创建单例,实现留 @x-otto/coding)。
|
|
63
|
+
*
|
|
64
|
+
* 静态 push 供给(register)+ 动态 pull 供给(registerProvider,服务 @file
|
|
65
|
+
* 大候选集懒查,RFC §1.3.1 F-domain / RFC-044 M044-07)。
|
|
66
|
+
*/
|
|
67
|
+
interface PluginInputRegistry {
|
|
68
|
+
/** 注册一批条目(last-write-wins,按 (label, kind) 复合键去重)。 */
|
|
69
|
+
register(entries: SigilEntry[]): void;
|
|
70
|
+
/**
|
|
71
|
+
* 注册某 sigil 的动态供给器(pull 模型,按 query 懒查,如 @file)。
|
|
72
|
+
* 返回注销函数。其结果在 searchAsync 中与静态条目合并。
|
|
73
|
+
*/
|
|
74
|
+
registerProvider(sigil: SigilPrefix, provider: SigilProvider): () => void;
|
|
75
|
+
/**
|
|
76
|
+
* 按 query + sigil 过滤匹配条目。过滤在此完成(R4),UI 只渲染结果。
|
|
77
|
+
* 命中数达 maxResults 立即返回,不全量遍历(F-domain 性能)。
|
|
78
|
+
* 仅静态条目;动态 provider 结果见 searchAsync。
|
|
79
|
+
*/
|
|
80
|
+
search(query: string, sigil?: SigilPrefix, maxResults?: number): SigilEntry[];
|
|
81
|
+
/**
|
|
82
|
+
* 异步搜索:静态条目(即时)+ 动态 provider 结果(await)合并去重。
|
|
83
|
+
* UI 应优先用此方法以纳入 @file 等懒查候选;无 provider 时等价 search。
|
|
84
|
+
*/
|
|
85
|
+
searchAsync(query: string, sigil?: SigilPrefix, maxResults?: number): Promise<SigilEntry[]>;
|
|
86
|
+
/** 返回某 sigil(或全部)的全量条目(只读快照)。 */
|
|
87
|
+
getAll(sigil?: SigilPrefix): readonly SigilEntry[];
|
|
88
|
+
/** 按 label 查首个匹配条目。 */
|
|
89
|
+
findByLabel(label: string): SigilEntry | undefined;
|
|
90
|
+
/** 注销某 label 的所有条目,返回是否删除过。 */
|
|
91
|
+
unregister(label: string): boolean;
|
|
92
|
+
/** 注销某 source 的所有条目(插件 unload 必调,防僵尸——R5)。 */
|
|
93
|
+
unregisterBySource(source: string): void;
|
|
94
|
+
}
|
|
95
|
+
/** 根据 kind 推断其归属的 sigil 前缀。 */
|
|
96
|
+
declare function sigilOf(kind: SigilKind): SigilPrefix;
|
|
97
|
+
/** sigil 触发信息:检测到的 sigil 字符 + 查询串 + 锚点位置。 */
|
|
98
|
+
interface SigilTrigger {
|
|
99
|
+
/** 触发的 sigil 字符。 */
|
|
100
|
+
sigil: SigilPrefix;
|
|
101
|
+
/** sigil 后的查询串(不含 sigil 字符本身)。 */
|
|
102
|
+
query: string;
|
|
103
|
+
/** sigil 字符在文本中的起始偏移(替换时从此处删到 cursorOffset)。 */
|
|
104
|
+
anchor: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 从光标前文本提取 sigil 触发。无触发返回 null。
|
|
108
|
+
* anchor 指向 sigil 字符位置(替换区间 = [anchor, cursorOffset))。
|
|
109
|
+
*/
|
|
110
|
+
declare function extractSigilPrefix(input: string, cursorOffset: number): SigilTrigger | null;
|
|
111
|
+
/**
|
|
112
|
+
* 设计问题 #5 / RFC-212 D1:判断当前触发是否落在「已被 Esc 驳回」的同一 sigil token 上。
|
|
113
|
+
* 为真则不应弹回面板——用户 Esc 关掉后继续在同一 `@`/`#` 上输入,不再打扰。
|
|
114
|
+
* 比较只看 sigil + anchor(token 身份)——anchor 由 `adjustDismissedSigil` 随编辑动态位移,
|
|
115
|
+
* 不是原始触发时的绝对位置,故本函数无需再关心"位置是否漂移"。
|
|
116
|
+
*/
|
|
117
|
+
declare function isSigilDismissed(trig: Pick<SigilTrigger, 'sigil' | 'anchor'>, dismissed: Pick<SigilTrigger, 'sigil' | 'anchor'> | null): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* RFC-212 D1:token 身份绑定的驳回记录。`tokenLength` 锚定在驳回快照时刻
|
|
120
|
+
* (= `cursorOffset - anchor`,即 1 + query.length),后续编辑不动态重算,
|
|
121
|
+
* 语义见 `adjustDismissedSigil` 的四类区间判定。
|
|
122
|
+
*/
|
|
123
|
+
interface DismissedSigil {
|
|
124
|
+
sigil: SigilPrefix;
|
|
125
|
+
anchor: number;
|
|
126
|
+
tokenLength: number;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* RFC-212 D1 状态机核心:给定旧驳回记录与本次文本编辑(oldText → newText),
|
|
130
|
+
* 计算编辑后应生效的驳回记录(`null` = 清态回 IDLE,编辑摧毁了 token 或与其无关)。
|
|
131
|
+
* 纯函数,不依赖 React state/ref,可独立单测。
|
|
132
|
+
*
|
|
133
|
+
* 判定依据编辑区间 `[start, end)`(old 坐标系)与 token 范围 `[anchor, anchor+tokenLength)`
|
|
134
|
+
* 的相对关系,四类穷举(`anchor < tokenEnd` 恒成立,`start <= end` 恒成立):
|
|
135
|
+
*
|
|
136
|
+
* 1. `end <= anchor`(编辑完全在 token 之前)→ 锚点位移:`anchor += delta`,tokenLength 不变。
|
|
137
|
+
* 2. `start >= tokenEnd`(编辑完全在 token 之后,"离开"场景)→ anchor/tokenLength 不变;
|
|
138
|
+
* 是否真的维持驳回态,由调用方比较新触发的 anchor 是否等于此处的 anchor 决定
|
|
139
|
+
* (`isSigilDismissed`)——本函数只负责"token 本身未被触碰"这一半的判定。
|
|
140
|
+
* 3. `start > anchor && start < tokenEnd`(编辑落在 token 内部、不含 `@`/`#` 字符本身)
|
|
141
|
+
* → 维持驳回态,anchor/tokenLength 均不变(不因编辑扩大 tokenLength——用户打字越过
|
|
142
|
+
* 快照边界即落入情形 2,交由新触发比较决定去留,而非线性扩张判定区间)。
|
|
143
|
+
* 4. 其余(`start <= anchor` 且与 token 有重叠,即编辑波及了 sigil 字符本身)
|
|
144
|
+
* → 清态回 IDLE(token 身份已被破坏)。
|
|
145
|
+
*/
|
|
146
|
+
declare function adjustDismissedSigil(dismissed: DismissedSigil | null, oldText: string, newText: string): DismissedSigil | null;
|
|
147
|
+
/**
|
|
148
|
+
* 用选中条目的 value 替换 [anchor, cursorOffset) 区间(即 `#query`/`@query`)。
|
|
149
|
+
* 返回新文本与新光标偏移(落在替换内容末尾)。
|
|
150
|
+
*
|
|
151
|
+
* 注意:value 原样注入(含换行/markdown 不校验,R2)。
|
|
152
|
+
* 调用方应走 PromptInput.update() 应用结果,以重算 detectMode(D7:# 替换后 mode 通常变 'send')。
|
|
153
|
+
*/
|
|
154
|
+
declare function applySigilCompletion(input: string, cursorOffset: number, anchor: number, entry: SigilEntry): {
|
|
155
|
+
text: string;
|
|
156
|
+
offset: number;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* RFC-212 D3:用选中条目的 value 替换一个**已存在 chip**的 `[chipStart, chipEnd)` 区间
|
|
160
|
+
* ——与 `applySigilCompletion`(从 anchor 插入,输入是"用户刚打的 @query")语义不同:
|
|
161
|
+
* 这里输入是"光标停靠在一个已展开的 chip 上,用户按 Enter 重新选择替换它",替换区间是
|
|
162
|
+
* chip 的实际边界,不涉及 anchor/query 概念。返回新文本与新光标偏移(落在替换内容末尾)。
|
|
163
|
+
*
|
|
164
|
+
* 注意:与 `applySigilCompletion` 不同,本函数不追加尾随空格——chip 替换场景两侧通常
|
|
165
|
+
* 已有边界(chip 本身的方括号,或前后文本),追加空格会破坏原有间距,交由调用方决定
|
|
166
|
+
* 是否需要额外分隔符。
|
|
167
|
+
*/
|
|
168
|
+
declare function applySigilReplacement(input: string, chipStart: number, chipEnd: number, entry: SigilEntry): {
|
|
169
|
+
text: string;
|
|
170
|
+
offset: number;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* RFC-215 D1:文本变更来源。每个修改输入框文本的路径必须声明自己是谁——
|
|
174
|
+
* 取代旧 `detectSigil: boolean`(普查发现 16 类调用点中 10 类语义错误,布尔无法
|
|
175
|
+
* 强迫调用方思考来源语义,见 RFC-215 §0.4)。
|
|
176
|
+
*
|
|
177
|
+
* sigil 触发准入的唯一判定:仅 `'typing'` 允许进入检测(`evaluateSigilTrigger`)。
|
|
178
|
+
* 新增来源归类不确定时选 `'programmatic'`(最保守:不触发任何检测)。
|
|
179
|
+
*/
|
|
180
|
+
type TextChangeSource = /** 用户逐字键入/删除(editKey 路径、IME 多字符 chunk)。 */'typing' /** 历史导航召回、搜索回填、/history 面板回填、外部编辑器写回、undo 回退、提交阻断跳转。 */ | 'programmatic' /** 粘贴插入(含短粘贴直插与大段折叠后插入)——RFC-215 D2:不触发 sigil 面板。 */ | 'paste'
|
|
181
|
+
/**
|
|
182
|
+
* ghost/typeahead/shell 补全接受、sigil 补全/删除/chip 替换回写。
|
|
183
|
+
* 当前对 sigil 检测的行为等价 `'programmatic'`(都不触发),预留语义位——
|
|
184
|
+
* 补全接受保留 typeahead 上下文语义、程序回填重置一切瞬态,未来分叉时不需改签名
|
|
185
|
+
* (RFC-215 D1,评审 F2:勿因"有值无独立消费"误删)。
|
|
186
|
+
*/
|
|
187
|
+
| 'completion';
|
|
188
|
+
/** `evaluateSigilTrigger` 的判定结果(RFC-215 D3)。 */
|
|
189
|
+
interface SigilTriggerDecision {
|
|
190
|
+
/**
|
|
191
|
+
* - `'open'`:产生新触发,调用方应设触发上下文并打开面板。
|
|
192
|
+
* - `'suppress'`:本次变更不产生新触发,也不主动关闭已开面板——调用方仅清触发上下文,
|
|
193
|
+
* 面板关闭交由既有失焦/Esc 机制(与旧 `!detectSigil` 分支行为一致)。
|
|
194
|
+
* - `'close'`:触发条件已消失(原 `@query` 被编辑破坏),调用方应清触发上下文并
|
|
195
|
+
* 通知宿主关闭面板(宿主未接线关闭回调时退化为 suppress 行为,向后兼容)。
|
|
196
|
+
*/
|
|
197
|
+
action: 'open' | 'suppress' | 'close';
|
|
198
|
+
/** action='open' 时的触发信息。 */
|
|
199
|
+
trigger?: SigilTrigger;
|
|
200
|
+
/** 经 `adjustDismissedSigil` 位移后的驳回态(任何来源都执行位移;open 时已清为 null)。 */
|
|
201
|
+
dismissed: DismissedSigil | null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* RFC-215 D3:sigil 触发准入状态机(纯函数)。与 `adjustDismissedSigil`(RFC-212 D1,
|
|
205
|
+
* 驳回态维护)合并为完整的一台触发机器——本函数管"是否触发",后者管"已驳回状态下
|
|
206
|
+
* 是否重新触发",调用顺序内聚在此,外部只消费 decision。
|
|
207
|
+
*
|
|
208
|
+
* 判定顺序(RFC-215 D3,六步):
|
|
209
|
+
* ① 位移驳回态(任何来源都执行——粘贴/回填同样会移动被驳回 token 的位置,
|
|
210
|
+
* 跳过位移会复发 RFC-212 缺陷 1;重要事项规则 3);
|
|
211
|
+
* ② 非 `'typing'` 来源 → suppress(历史导航/粘贴/补全接受不触发面板——本 RFC 的 bug 修复本体);
|
|
212
|
+
* ③ 文本非增长 → suppress(保留既有 `grew` 优化语义:删除/替换不产生新触发);
|
|
213
|
+
* ④ 无 sigil 触发命中(或被 `sigilFilter` 过滤)→ close(原触发被编辑破坏,面板应关闭);
|
|
214
|
+
* ⑤ 触发命中但落在已驳回 token 上 → suppress(Esc 驳回后继续输入不再打扰);
|
|
215
|
+
* ⑥ 触发命中且未被驳回 → open + 清驳回态。
|
|
216
|
+
*
|
|
217
|
+
* `sigilFilter` 缺省 `(sigil) => sigil === '@'`——与 TUI 现状的 @-only 硬过滤一致
|
|
218
|
+
* (`#` 走 memory 模式检测,不在本函数管辖;评审 F4-2)。
|
|
219
|
+
*/
|
|
220
|
+
declare function evaluateSigilTrigger(input: {
|
|
221
|
+
source: TextChangeSource;
|
|
222
|
+
oldText: string;
|
|
223
|
+
newText: string;
|
|
224
|
+
cursorOffset: number;
|
|
225
|
+
dismissed: DismissedSigil | null;
|
|
226
|
+
sigilFilter?: (sigil: SigilPrefix) => boolean;
|
|
227
|
+
}): SigilTriggerDecision;
|
|
228
|
+
//#endregion
|
|
229
|
+
export { DismissedSigil, PluginInputRegistry, SigilEntry, SigilKind, SigilPrefix, SigilProvider, SigilResolveContext, SigilResolver, SigilSource, SigilTrigger, SigilTriggerDecision, TextChangeSource, adjustDismissedSigil, applySigilCompletion, applySigilReplacement, evaluateSigilTrigger, extractSigilPrefix, isSigilDismissed, sigilOf };
|
|
230
|
+
//# sourceMappingURL=input.d.ts.map
|
package/dist/input.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){return e===`hashtag`?`#`:`@`}const t=/(?:^|\s)([#@])(\S*)$/;function n(e,n){let r=e.slice(0,n).match(t);if(!r)return null;let i=r[1],a=r[2]??``;return{sigil:i,query:a,anchor:n-a.length-1}}function r(e,t){return!!t&&t.sigil===e.sigil&&t.anchor===e.anchor}function i(e,t){let n=Math.min(e.length,t.length),r=0;for(;r<n&&e[r]===t[r];)r++;let i=n-r,a=0;for(;a<i&&e[e.length-1-a]===t[t.length-1-a];)a++;return{start:r,end:e.length-a,delta:t.length-e.length}}function a(e,t,n){if(!e||t===n)return e;let{start:r,end:a,delta:o}=i(t,n),s=e.anchor+e.tokenLength;return a<=e.anchor?{...e,anchor:e.anchor+o}:r>=s||r>e.anchor&&r<s?e:null}function o(e,t,n,r){if(n<0||n>t)return{text:e,offset:t};let i=t>=e.length?` `:``;return{text:e.slice(0,n)+r.value+i+e.slice(t),offset:n+r.value.length+(i?1:0)}}function s(e,t,n,r){return t<0||t>n||n>e.length?{text:e,offset:n}:{text:e.slice(0,t)+r.value+e.slice(n),offset:t+r.value.length}}function c(e){let{source:t,oldText:i,newText:o,cursorOffset:s}=e,c=e.sigilFilter??(e=>e===`@`),l=a(e.dismissed,i,o);if(t!==`typing`||o.length<=i.length)return{action:`suppress`,dismissed:l};let u=n(o,s),d=u&&c(u.sigil)?u:null;return d?r(d,l)?{action:`suppress`,dismissed:l}:{action:`open`,trigger:d,dismissed:null}:{action:`close`,dismissed:l}}export{a as adjustDismissedSigil,o as applySigilCompletion,s as applySigilReplacement,c as evaluateSigilTrigger,n as extractSigilPrefix,r as isSigilDismissed,e as sigilOf};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
//#region src/message.d.ts
|
|
2
|
+
/** Message: AgentMessage/UserMessage/SystemMessage interchange types. */
|
|
3
|
+
type Timestamp = number;
|
|
4
|
+
type KnownApi = 'openai-completions' | 'openai-responses' | 'azure-openai-responses' | 'openai-codex-responses' | 'anthropic-messages' | 'bedrock-converse-stream' | 'google-generative-ai' | 'google-gemini-cli' | 'google-vertex';
|
|
5
|
+
type Api = KnownApi | (string & {});
|
|
6
|
+
type KnownProvider = 'anthropic' | 'openai' | 'google' | 'amazon-bedrock' | 'github-copilot' | 'xai' | 'groq' | 'openrouter' | 'deepseek' | 'ollama' | 'moonshot' | 'custom';
|
|
7
|
+
type Provider = KnownProvider | (string & {});
|
|
8
|
+
type StopReason = 'end_turn' | 'max_tokens' | 'tool_use' | 'stop_sequence' | 'refusal';
|
|
9
|
+
interface Usage {
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
cacheReadTokens: number;
|
|
13
|
+
cacheWriteTokens: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 结构化的 zod 兼容垫片——协议层不直接依赖 zod,靠结构类型让 provider/工具定义喂入真 zod schema。
|
|
17
|
+
*/
|
|
18
|
+
interface ZodType<T = unknown> {
|
|
19
|
+
parse(data: unknown): T;
|
|
20
|
+
safeParse(data: unknown): {
|
|
21
|
+
success: boolean;
|
|
22
|
+
data?: T;
|
|
23
|
+
error?: unknown;
|
|
24
|
+
};
|
|
25
|
+
toJSONSchema?: () => unknown;
|
|
26
|
+
}
|
|
27
|
+
interface TextContent {
|
|
28
|
+
type: 'text';
|
|
29
|
+
text: string;
|
|
30
|
+
signature?: string;
|
|
31
|
+
}
|
|
32
|
+
interface ImageContent {
|
|
33
|
+
type: 'image';
|
|
34
|
+
mime: string;
|
|
35
|
+
source: string;
|
|
36
|
+
}
|
|
37
|
+
interface ThinkingContent {
|
|
38
|
+
type: 'thinking';
|
|
39
|
+
text: string;
|
|
40
|
+
signature?: string;
|
|
41
|
+
}
|
|
42
|
+
interface ToolCall {
|
|
43
|
+
type: 'tool_call';
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
arguments: Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* 流式参数 JSON 解析失败标记(RFC-142 D4)——`stopReason: max_tokens` 截断半截
|
|
49
|
+
* JSON 时 `parseToolArguments` 置位,runtime 责任链据此把该调用转为明确错误
|
|
50
|
+
* tool_result 而非带空参数进入 zod 校验(与污染类错误分离错误通道)。
|
|
51
|
+
*
|
|
52
|
+
* **进程内标记,不进 wire**:各 provider 序列化发给模型时均为显式字段拾取
|
|
53
|
+
* (`{type, id, name, input: arguments}`,无 spread),本字段天然剥除——有测试钉住
|
|
54
|
+
* (RFC-142 重要事项规则4)。
|
|
55
|
+
*/
|
|
56
|
+
argumentsParseFailed?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A2UI content block(RFC-105 D6/M105-5b):agent 生成的声明式 UI payload。
|
|
60
|
+
* 对齐 a2ui-project/a2ui(v0.9)的核心哲学——扁平组件列表 + ID 引用 + 受信组件 catalog、
|
|
61
|
+
* 纯数据非代码、未知组件类型文本降级。
|
|
62
|
+
*
|
|
63
|
+
* 组件类型(TUI 终端安全子集,首阶段只读 display):
|
|
64
|
+
* - Text:纯文本(text 字段)。
|
|
65
|
+
* - Card:带标题容器(title + children ID 引用)。
|
|
66
|
+
* - List:条目列表(items ID 引用 + ordered 标志)。
|
|
67
|
+
* - Progress:ASCII 进度条(value 0-100,label 可选)。
|
|
68
|
+
* - Chip:内联彩色标签(text + color)。
|
|
69
|
+
* - Button:交互按钮(RFC-105 D6 F5/M105-5c,数字键触发 a2ui action;只读 display 通过
|
|
70
|
+
* renderA2uiBlock 提取为 A2uiButtonInfo 元数据)。
|
|
71
|
+
* - Form:交互式表单(RFC-211 M1:字段定义 + TUI 弹窗填写 + web-ui 原生表单)。
|
|
72
|
+
* 字段类型 text/select/toggle,payload 原样回流,不可执行代码。
|
|
73
|
+
* 未知 type → 文本摘要表示(不崩,fail-soft,R7)。
|
|
74
|
+
*/
|
|
75
|
+
interface A2uiComponent {
|
|
76
|
+
id: string;
|
|
77
|
+
type: 'Text' | 'Card' | 'List' | 'Progress' | 'Chip' | 'Button' | 'Form' | (string & {});
|
|
78
|
+
text?: string;
|
|
79
|
+
title?: string;
|
|
80
|
+
children?: string[];
|
|
81
|
+
items?: string[];
|
|
82
|
+
ordered?: boolean;
|
|
83
|
+
value?: number;
|
|
84
|
+
max?: number;
|
|
85
|
+
label?: string;
|
|
86
|
+
color?: 'accent' | 'green' | 'amber' | 'red' | 'gray';
|
|
87
|
+
action?: string;
|
|
88
|
+
shortcut?: string;
|
|
89
|
+
/** 表单标题(弹窗标题 + 对话流摘要标题)。 */
|
|
90
|
+
formTitle?: string;
|
|
91
|
+
/** 表单字段定义。 */
|
|
92
|
+
fields?: A2uiFormField[];
|
|
93
|
+
}
|
|
94
|
+
/** 单个表单字段(`A2uiComponent.type === 'Form'` 时使用)。 */
|
|
95
|
+
interface A2uiFormField {
|
|
96
|
+
/** 字段 id(Form 内唯一,submit payload 的 key)。 */
|
|
97
|
+
id: string;
|
|
98
|
+
/** 字段类型。 */
|
|
99
|
+
type: 'text' | 'select' | 'toggle';
|
|
100
|
+
/** 显示标签。 */
|
|
101
|
+
label: string;
|
|
102
|
+
/** 默认值(text=字符串, select=option id, toggle=boolean)。 */
|
|
103
|
+
defaultValue?: string | boolean;
|
|
104
|
+
/** 占位符(text 类型)。 */
|
|
105
|
+
placeholder?: string;
|
|
106
|
+
/** 选项列表(select 类型必填)。 */
|
|
107
|
+
options?: Array<{
|
|
108
|
+
id: string;
|
|
109
|
+
label: string;
|
|
110
|
+
}>;
|
|
111
|
+
/** 是否必填(渲染提示,不做前端校验——非目标)。 */
|
|
112
|
+
required?: boolean;
|
|
113
|
+
}
|
|
114
|
+
interface A2uiContent {
|
|
115
|
+
type: 'a2ui';
|
|
116
|
+
/** A2UI 协议版本(v0.9 snapshot),供未来版本迁移用。 */
|
|
117
|
+
version: string;
|
|
118
|
+
/** 扁平组件列表,ID 引用构建组件树,增量子集可追加/替换。 */
|
|
119
|
+
components: A2uiComponent[];
|
|
120
|
+
}
|
|
121
|
+
type ContentPart = TextContent | ImageContent | ThinkingContent | ToolCall | A2uiContent;
|
|
122
|
+
interface UserMessage {
|
|
123
|
+
role: 'user';
|
|
124
|
+
content: string | ContentPart[];
|
|
125
|
+
timestamp: Timestamp;
|
|
126
|
+
uuid?: string;
|
|
127
|
+
/**
|
|
128
|
+
* RFC-050 REV-1:引擎注入的内部消息(如预算收尾 steering、截断续写提示),模型需要看到、
|
|
129
|
+
* 但**不应渲染给用户**(否则 resume 时显示成用户从未输入的发言)。provider 序列化忽略此字段。
|
|
130
|
+
*/
|
|
131
|
+
internal?: boolean;
|
|
132
|
+
}
|
|
133
|
+
interface AssistantMessage {
|
|
134
|
+
role: 'assistant';
|
|
135
|
+
/**
|
|
136
|
+
* 消息内容。A2uiContent 是前向兼容新增(RFC-155 M2 结案后的协议层收口):
|
|
137
|
+
* provider 序列化层仅消费 text/tool_call/thinking,a2ui 块被静默跳过不出进 LLM 请求体;
|
|
138
|
+
* 渲染层(message-lines.ts)经 `renderA2uiBlock` 渲染。
|
|
139
|
+
*/
|
|
140
|
+
content: (TextContent | ThinkingContent | ToolCall | A2uiContent)[];
|
|
141
|
+
api: Api;
|
|
142
|
+
provider: Provider;
|
|
143
|
+
model: string;
|
|
144
|
+
usage: Usage;
|
|
145
|
+
stopReason: StopReason;
|
|
146
|
+
uuid?: string;
|
|
147
|
+
}
|
|
148
|
+
interface ToolResultMessage<T extends unknown = unknown> {
|
|
149
|
+
role: 'tool_result';
|
|
150
|
+
toolCallId: string;
|
|
151
|
+
toolName: string;
|
|
152
|
+
/**
|
|
153
|
+
* 工具结果内容。A2uiContent 是前向兼容新增(RFC-155 M2 结案后的协议层收口):
|
|
154
|
+
* provider 序列化仅消费 text,a2ui 块被静默跳过;渲染层(message-lines.ts)经
|
|
155
|
+
* `renderA2uiBlock` 渲染为终端卡片。
|
|
156
|
+
*/
|
|
157
|
+
content: (TextContent | ImageContent | A2uiContent)[];
|
|
158
|
+
details: T;
|
|
159
|
+
isError: boolean;
|
|
160
|
+
timestamp: Timestamp;
|
|
161
|
+
uuid?: string;
|
|
162
|
+
/** RFC-050 REV-1:引擎合成的内部 tool_result(如预算耗尽未执行的占位),不渲染给用户。 */
|
|
163
|
+
internal?: boolean;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* RFC-144 M1:mid-conversation system 通知消息——**真正进 provider 请求**的协议消息类型
|
|
167
|
+
* (区别于 `@x-otto/hook-contracts` 的 `SystemAgentMessage`,后者是 hooks/UI 域、
|
|
168
|
+
* convertToLLM 类型边界显式过滤、绝不进 provider 请求)。
|
|
169
|
+
*
|
|
170
|
+
* 用途:lifecycleAsync 工具(RFC-144)真正完成时,追加一条独立消息告知 LLM"运营方观察到
|
|
171
|
+
* 的状态变化"(Anthropic mid-conversation system message 官方场景之一),不修改/替换任何
|
|
172
|
+
* 历史消息(纯追加语义),LLM 在下一轮推理中作为权威事实处理。
|
|
173
|
+
*
|
|
174
|
+
* provider 序列化:
|
|
175
|
+
* - Anthropic(原生支持,Claude Opus 4.8+)→ `{role: 'system', content}` 追加进 messages 数组,
|
|
176
|
+
* 放置规则见 RFC-144 重要事项规则 5(必须紧跟携带 tool_result 的 user turn 之后)。
|
|
177
|
+
* - 不支持该特性的模型/provider(如 OpenAI,或 Anthropic 旧模型)→ 降级为等价的
|
|
178
|
+
* `role: 'user'` 消息 + 显式文本前缀(牺牲"运营方权威"语义,保留基本可用性),
|
|
179
|
+
* 降级逻辑归属 provider adapter 层(本类型定义不关心降级细节)。
|
|
180
|
+
*
|
|
181
|
+
* 不可变性(RFC-144 禁区):一旦追加,绝不编辑或删除已发送的 system 通知——结果更新一律
|
|
182
|
+
* 追加新消息,不改写旧消息(Anthropic 官方限制:编辑会使该点之后的 prompt cache 失效)。
|
|
183
|
+
*/
|
|
184
|
+
interface SystemNotificationMessage {
|
|
185
|
+
role: 'system_notification';
|
|
186
|
+
/** 通知正文(如 "[Tool completion] pnpm build finished: exit 0")。 */
|
|
187
|
+
content: string;
|
|
188
|
+
/**
|
|
189
|
+
* 该通知对应的工具执行是否失败——显式字段,供消费方(渲染层/审计)直接读取,不必解析
|
|
190
|
+
* `content` 文案判断状态(RFC-144 M2:文案格式可能演进,靠字符串匹配脆弱且易误判)。
|
|
191
|
+
*/
|
|
192
|
+
isError?: boolean;
|
|
193
|
+
/** 关联的 lifecycleAsync 工具调用 id,供消费方(渲染层/审计)溯源。 */
|
|
194
|
+
relatedToolCallId?: string;
|
|
195
|
+
timestamp: Timestamp;
|
|
196
|
+
uuid?: string;
|
|
197
|
+
}
|
|
198
|
+
type Message = UserMessage | AssistantMessage | ToolResultMessage | SystemNotificationMessage;
|
|
199
|
+
/**
|
|
200
|
+
* 历史压缩摘要消息的标记前缀——单一真相源(跨包协议常量)。
|
|
201
|
+
* 生产者 @x-otto/memory 注入摘要消息时用、压缩去重(isSummaryMessage)时识别;消费者
|
|
202
|
+
* @x-otto/agent 的 ContextPipeline 从会话树恢复摘要时用同一前缀重建摘要消息。放在协议叶子,
|
|
203
|
+
* 避免双份字面量漂移导致 isSummaryMessage 失配。
|
|
204
|
+
*/
|
|
205
|
+
declare const CONVERSATION_SUMMARY_PREFIX = "[Conversation Summary]";
|
|
206
|
+
/**
|
|
207
|
+
* 从 assistant 消息的内容块中提取全部 tool_call 项(终局架构 review 2026-07-10 P0-3:
|
|
208
|
+
* 从 @x-otto/ai 物理迁移——此前是 @x-otto/ai 唯一自己实现(非 re-export)的两个函数,只依赖
|
|
209
|
+
* AssistantMessage/ToolCall,真源本就该在协议叶子而非模型注册中枢)。
|
|
210
|
+
*/
|
|
211
|
+
declare function getToolCallsByAssistantMessage(message: AssistantMessage): ToolCall[];
|
|
212
|
+
/** 该 assistant 消息是否含至少一个 tool_call。 */
|
|
213
|
+
declare function hasToolCalls(message: AssistantMessage): boolean;
|
|
214
|
+
//#endregion
|
|
215
|
+
export { A2uiComponent, A2uiContent, A2uiFormField, Api, AssistantMessage, CONVERSATION_SUMMARY_PREFIX, ContentPart, ImageContent, KnownApi, KnownProvider, Message, Provider, StopReason, SystemNotificationMessage, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, ZodType, getToolCallsByAssistantMessage, hasToolCalls };
|
|
216
|
+
//# sourceMappingURL=message.d.ts.map
|
package/dist/message.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=`[Conversation Summary]`;function t(e){return e.content.filter(e=>e.type===`tool_call`)}function n(e){return e.content.some(e=>e.type===`tool_call`)}export{e as CONVERSATION_SUMMARY_PREFIX,t as getToolCallsByAssistantMessage,n as hasToolCalls};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-otto/interchange",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist"
|
|
6
6
|
],
|
|
@@ -11,6 +11,22 @@
|
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./message": {
|
|
16
|
+
"types": "./dist/message.d.ts",
|
|
17
|
+
"import": "./dist/message.js"
|
|
18
|
+
},
|
|
19
|
+
"./input": {
|
|
20
|
+
"types": "./dist/input.d.ts",
|
|
21
|
+
"import": "./dist/input.js"
|
|
22
|
+
},
|
|
23
|
+
"./image": {
|
|
24
|
+
"types": "./dist/image.d.ts",
|
|
25
|
+
"import": "./dist/image.js"
|
|
26
|
+
},
|
|
27
|
+
"./context-fragment": {
|
|
28
|
+
"types": "./dist/context-fragment.d.ts",
|
|
29
|
+
"import": "./dist/context-fragment.js"
|
|
14
30
|
}
|
|
15
31
|
},
|
|
16
32
|
"publishConfig": {
|