@yeaft/webchat-agent 0.1.591 → 0.1.592

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.591",
3
+ "version": "0.1.592",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -45,6 +45,31 @@ function thinkingV1Enabled() {
45
45
  return process.env.UNIFY_THINKING_V1 === '1';
46
46
  }
47
47
 
48
+ /**
49
+ * task-DESIGN-v4: Chat Completions adapter is deprecated in favour of
50
+ * `openai-responses.js` (Responses API) for OpenAI-protocol providers and
51
+ * `anthropic.js` for Anthropic. This warning fires once per process the
52
+ * first time the adapter is instantiated, unless UNIFY_SUPPRESS_DEPRECATION=1.
53
+ * Removal is scheduled for Phase 7 of the multi-VP redesign — see
54
+ * `agent/unify/DESIGN.md` § "Migration Plan".
55
+ */
56
+ let _chatCompletionsDeprecationWarned = false;
57
+ function warnChatCompletionsDeprecated() {
58
+ if (_chatCompletionsDeprecationWarned) return;
59
+ if (process.env.UNIFY_SUPPRESS_DEPRECATION === '1') {
60
+ _chatCompletionsDeprecationWarned = true;
61
+ return;
62
+ }
63
+ _chatCompletionsDeprecationWarned = true;
64
+ // eslint-disable-next-line no-console
65
+ console.warn(
66
+ '[unify] ChatCompletionsAdapter is deprecated. Migrate OpenAI-protocol '
67
+ + 'providers to the Responses API (set provider.protocol="openai-responses"). '
68
+ + 'This adapter will be removed in a future release. Set '
69
+ + 'UNIFY_SUPPRESS_DEPRECATION=1 to silence this warning.'
70
+ );
71
+ }
72
+
48
73
  /**
49
74
  * Check if a model ID is an OpenAI model that supports max_completion_tokens.
50
75
  * OpenAI introduced max_completion_tokens with o1 and made it standard for
@@ -79,6 +104,7 @@ export class ChatCompletionsAdapter extends LLMAdapter {
79
104
  super({ apiKey, baseUrl });
80
105
  this.#apiKey = apiKey;
81
106
  this.#baseUrl = baseUrl.replace(/\/+$/, ''); // strip trailing slash
107
+ warnChatCompletionsDeprecated();
82
108
  }
83
109
 
84
110
  /** Expose baseUrl for testing. */
@@ -33,9 +33,34 @@ import {
33
33
  LLMServerError,
34
34
  LLMAbortError,
35
35
  } from './adapter.js';
36
+ import {
37
+ normalizeEffort,
38
+ getThinkingCapability,
39
+ } from '../models.js';
36
40
 
37
41
  const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
38
42
 
43
+ /**
44
+ * Feature-flag accessor mirroring anthropic.js. UNIFY_THINKING_V1 is OFF by
45
+ * default; set env to '1' to enable thinking-mode field translation. Read
46
+ * lazily so tests can flip the flag between calls.
47
+ */
48
+ function thinkingV1Enabled() {
49
+ return process.env.UNIFY_THINKING_V1 === '1';
50
+ }
51
+
52
+ /**
53
+ * Translate a normalised effort ('low'|'medium'|'high'|'max') into the value
54
+ * accepted by the OpenAI Responses `reasoning.effort` field. Responses today
55
+ * accepts 'low'|'medium'|'high' — 'max' degrades to 'high' to match the
56
+ * registry's normaliseEffort downgrade rule.
57
+ */
58
+ function effortForResponses(effort) {
59
+ if (!effort) return null;
60
+ if (effort === 'max') return 'high';
61
+ return effort;
62
+ }
63
+
39
64
  export class OpenAIResponsesAdapter extends LLMAdapter {
40
65
  #apiKey;
41
66
  #baseUrl;
@@ -200,9 +225,9 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
200
225
  // ─── Streaming ──────────────────────────────────────────
201
226
 
202
227
  /**
203
- * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, extraBody?: object, signal?: AbortSignal }} params
228
+ * @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', extraBody?: object, signal?: AbortSignal }} params
204
229
  */
205
- async *stream({ model, system, messages, tools, maxTokens = 16384, extraBody, signal }) {
230
+ async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal }) {
206
231
  if (signal?.aborted) throw new LLMAbortError();
207
232
 
208
233
  const body = {
@@ -214,6 +239,20 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
214
239
  if (system) body.instructions = system;
215
240
  const translatedTools = this.#translateTools(tools);
216
241
  if (translatedTools) body.tools = translatedTools;
242
+
243
+ // Inject Responses-API thinking-mode field. Mirrors anthropic.js gating:
244
+ // feature flag must be on, effort must be a known value, and the model's
245
+ // registry entry must declare thinkingProtocol === 'openai-reasoning'.
246
+ // Unknown / unsupported models silently drop the field.
247
+ const normEffort = normalizeEffort(effort);
248
+ if (thinkingV1Enabled() && normEffort) {
249
+ const cap = getThinkingCapability(model);
250
+ if (cap.supportsThinking && cap.thinkingProtocol === 'openai-reasoning') {
251
+ const wireEffort = effortForResponses(normEffort);
252
+ if (wireEffort) body.reasoning = { effort: wireEffort };
253
+ }
254
+ }
255
+
217
256
  if (extraBody) Object.assign(body, extraBody);
218
257
 
219
258
  let response;
@@ -375,7 +414,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
375
414
 
376
415
  // ─── Non-streaming call() ───────────────────────────────
377
416
 
378
- async call({ model, system, messages, maxTokens = 4096, extraBody, signal }) {
417
+ async call({ model, system, messages, maxTokens = 4096, effort, extraBody, signal }) {
379
418
  if (signal?.aborted) throw new LLMAbortError();
380
419
 
381
420
  const body = {
@@ -384,6 +423,17 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
384
423
  max_output_tokens: maxTokens,
385
424
  };
386
425
  if (system) body.instructions = system;
426
+
427
+ // Mirror stream()'s thinking injection for non-streaming side queries.
428
+ const normEffort = normalizeEffort(effort);
429
+ if (thinkingV1Enabled() && normEffort) {
430
+ const cap = getThinkingCapability(model);
431
+ if (cap.supportsThinking && cap.thinkingProtocol === 'openai-reasoning') {
432
+ const wireEffort = effortForResponses(normEffort);
433
+ if (wireEffort) body.reasoning = { effort: wireEffort };
434
+ }
435
+ }
436
+
387
437
  if (extraBody) Object.assign(body, extraBody);
388
438
 
389
439
  let response;
package/unify/prompts.js CHANGED
@@ -158,6 +158,11 @@ const RAW_TEMPLATES = {
158
158
  modeUnified: readTemplate('mode-unified.md'),
159
159
  modeDream: readTemplate('mode-dream.md'),
160
160
  toolGuidance: readTemplate('tool-guidance.md'),
161
+ // Phase 1 — DESIGN.md "Migration Plan" harness fragments. Optional so
162
+ // older deployments without the templates still boot; buildWorkerPrompt /
163
+ // buildRouterPrompt callers will simply omit the section.
164
+ harnessWorkerShape: readTemplate('harness/worker-shape.md', { required: false }),
165
+ harnessRouterShape: readTemplate('harness/router-shape.md', { required: false }),
161
166
  };
162
167
 
163
168
  /**
@@ -637,3 +642,164 @@ function renderCoreMemory(coreMemory, lang, memoryTraceAvailable) {
637
642
  }
638
643
  return lines.join('\n');
639
644
  }
645
+
646
+ // ─── Phase 1: Worker / Router prompt splits ──────────────────────
647
+ //
648
+ // DESIGN.md (multi-VP redesign) describes two distinct prompt shapes:
649
+ //
650
+ // • Worker prompt — what a VP sees when it executes a turn. Layered as
651
+ // A (identity + summaries) / B (router-preselected memory) / C (task
652
+ // scope) / D (turn scope).
653
+ // • Router prompt — what the per-VP Router sees before it decides
654
+ // plans[]. Identity-summary layer + recent group state, no task /
655
+ // turn-scope detail.
656
+ //
657
+ // To stay backwards-compatible with existing callers we KEEP
658
+ // `buildSystemPrompt` and treat the two new entry points as thin wrappers
659
+ // that:
660
+ // 1) compose Layer-A summaries (user / group / vp) into the right
661
+ // headed sections, and
662
+ // 2) prepend the matching harness/*-shape.md fragment when present.
663
+ //
664
+ // Subsequent phases will migrate engine.js / router.js to these entry
665
+ // points and start filling Layers B / C with the new memory tree. For
666
+ // now they exist primarily so tests can pin the contract.
667
+
668
+ const LAYER_A_HEADERS = {
669
+ en: {
670
+ user: '## summary_user',
671
+ group: '## summary_group',
672
+ vp: '## summary_vp',
673
+ },
674
+ zh: {
675
+ user: '## 用户总结',
676
+ group: '## 群组总结',
677
+ vp: '## VP 总结',
678
+ },
679
+ };
680
+
681
+ /**
682
+ * Render Layer A's three rolling summaries (user / group / vp). Each is
683
+ * optional; missing or empty strings are skipped. Headers follow the
684
+ * `## summary_<scope>` convention so Layer-B/C/D headers don't collide.
685
+ *
686
+ * @param {{user?: string, group?: string, vp?: string}} summaries
687
+ * @param {'en'|'zh'} language
688
+ * @returns {string} concatenated block ('' when nothing to render)
689
+ */
690
+ export function renderLayerASummaries(summaries, language = 'en') {
691
+ if (!summaries || typeof summaries !== 'object') return '';
692
+ const headers = LAYER_A_HEADERS[language] || LAYER_A_HEADERS.en;
693
+ const out = [];
694
+ for (const key of ['user', 'group', 'vp']) {
695
+ const body = typeof summaries[key] === 'string' ? summaries[key].trim() : '';
696
+ if (!body) continue;
697
+ out.push(`${headers[key]}\n${body}`);
698
+ }
699
+ return out.join('\n\n');
700
+ }
701
+
702
+ /**
703
+ * Worker prompt entry point (DESIGN.md Phase 1).
704
+ *
705
+ * Layered output:
706
+ * harness/worker-shape — what each layer means (optional fragment)
707
+ * Layer A — buildSystemPrompt(...) output (identity + persona + Layer-A
708
+ * summaries via `summaries`)
709
+ * Layer B — `preselectedMemory` block (router-supplied)
710
+ * Layer C — `taskScope` block (active task summary + related-task window)
711
+ * Layer D — `turnScope` block (inbound envelope, in-flight turn notes)
712
+ *
713
+ * Layers B/C/D are passed in as already-rendered strings so this builder
714
+ * stays free of memory-store / task-store IO. Phase 2/3 will provide the
715
+ * real renderers; for now any caller can stub them.
716
+ *
717
+ * @param {{
718
+ * language?: 'en'|'zh',
719
+ * summaries?: {user?: string, group?: string, vp?: string},
720
+ * preselectedMemory?: string,
721
+ * taskScope?: string,
722
+ * turnScope?: string,
723
+ * includeShape?: boolean,
724
+ * ...rest: import('./prompts.js').buildSystemPrompt
725
+ * }} params
726
+ * @returns {string}
727
+ */
728
+ export function buildWorkerPrompt(params = {}) {
729
+ const {
730
+ language = 'en',
731
+ summaries,
732
+ preselectedMemory,
733
+ taskScope,
734
+ turnScope,
735
+ includeShape = true,
736
+ ...rest
737
+ } = params;
738
+
739
+ const parts = [];
740
+
741
+ // Optional harness — describes the layered shape.
742
+ if (includeShape) {
743
+ const shape = getTemplate('harnessWorkerShape', language);
744
+ if (shape) parts.push(shape);
745
+ }
746
+
747
+ // Layer A — base + persona + summaries.
748
+ const baseBlock = buildSystemPrompt({ ...rest, language });
749
+ if (baseBlock) parts.push(baseBlock);
750
+ const summaryBlock = renderLayerASummaries(summaries, language);
751
+ if (summaryBlock) parts.push(summaryBlock);
752
+
753
+ // Layer B — router-preselected memory entries (rendered upstream).
754
+ if (typeof preselectedMemory === 'string' && preselectedMemory.trim()) {
755
+ parts.push(preselectedMemory.trim());
756
+ }
757
+
758
+ // Layer C — task scope.
759
+ if (typeof taskScope === 'string' && taskScope.trim()) {
760
+ parts.push(taskScope.trim());
761
+ }
762
+
763
+ // Layer D — turn scope (inbound envelope, in-flight turn notes).
764
+ if (typeof turnScope === 'string' && turnScope.trim()) {
765
+ parts.push(turnScope.trim());
766
+ }
767
+
768
+ return parts.join('\n\n');
769
+ }
770
+
771
+ /**
772
+ * Router prompt entry point (DESIGN.md Phase 1).
773
+ *
774
+ * The Router sees identity context (no persona — it speaks as a routing
775
+ * brain, not as any specific VP), the three Layer-A summaries, and a
776
+ * `routerContext` block prepared upstream (group roster, recent turns,
777
+ * pending tasks). Output schema is enforced by the harness fragment.
778
+ *
779
+ * @param {{
780
+ * language?: 'en'|'zh',
781
+ * summaries?: {user?: string, group?: string, vp?: string},
782
+ * routerContext?: string,
783
+ * includeShape?: boolean,
784
+ * }} params
785
+ * @returns {string}
786
+ */
787
+ export function buildRouterPrompt(params = {}) {
788
+ const { language = 'en', summaries, routerContext, includeShape = true } = params;
789
+ const parts = [];
790
+
791
+ if (includeShape) {
792
+ const shape = getTemplate('harnessRouterShape', language);
793
+ if (shape) parts.push(shape);
794
+ }
795
+
796
+ const summaryBlock = renderLayerASummaries(summaries, language);
797
+ if (summaryBlock) parts.push(summaryBlock);
798
+
799
+ if (typeof routerContext === 'string' && routerContext.trim()) {
800
+ parts.push(routerContext.trim());
801
+ }
802
+
803
+ return parts.join('\n\n');
804
+ }
805
+
@@ -0,0 +1,49 @@
1
+ <!-- lang:en -->
2
+ # Prompt Shape (Router)
3
+
4
+ You are the per-VP Router. You see the group's roster, summaries, recent
5
+ turns, and the latest user message. You return a JSON `plans[]` array — one
6
+ plan per VP that should act this turn, in execution order.
7
+
8
+ Each plan contains:
9
+
10
+ - `vpId` — which VP runs.
11
+ - `forwardQuery` — `{ userOriginal, intent }`. `userOriginal` is the
12
+ verbatim user text; `intent` is a one-line gloss in third person. Do not
13
+ rewrite the user's words; the worker will read both.
14
+ - `preselect` — `{ memoryPaths[], taskIds[] }`. Memory paths are
15
+ scope-prefixed (`user/`, `groups/<id>/`, `vp/<id>/`, `tasks/<id>/`).
16
+ - `thinking` — `null | "high" | "max"`. Set when the turn warrants
17
+ deeper reasoning; leave `null` to use the VP / global default.
18
+ - `thinkingReason` — short justification when `thinking` is non-null.
19
+
20
+ Hard rules:
21
+ - Never include `vp/<other>/` paths in `preselect.memoryPaths`. Cross-VP
22
+ private memory is hard-blocked.
23
+ - Plans run sequentially in the order returned. Treat ordering as load
24
+ bearing; the second plan can read the first plan's output.
25
+ - If no VP needs to act, return `{"plans": []}`.
26
+ <!-- lang:zh -->
27
+ # Prompt 结构(Router)
28
+
29
+ 你是当前群组的 Router。你能看到群成员、总结、最近的回合,以及最新的用户
30
+ 消息。你返回一个 JSON `plans[]` 数组——每个需要发言的 VP 一个 plan,按
31
+ 执行顺序排列。
32
+
33
+ 每个 plan 包含:
34
+
35
+ - `vpId`:要执行的 VP。
36
+ - `forwardQuery`:`{ userOriginal, intent }`。`userOriginal` 是用户的
37
+ 原话;`intent` 是用第三人称写的一行意图说明。不要改写用户原话,Worker
38
+ 会同时看到两者。
39
+ - `preselect`:`{ memoryPaths[], taskIds[] }`。memoryPaths 必须带 scope
40
+ 前缀(`user/`、`groups/<id>/`、`vp/<id>/`、`tasks/<id>/`)。
41
+ - `thinking`:`null | "high" | "max"`。需要深度推理时设置,否则保持 null
42
+ 使用 VP / 全局默认。
43
+ - `thinkingReason`:当 `thinking` 非空时的简短理由。
44
+
45
+ 硬规则:
46
+ - `preselect.memoryPaths` 不允许包含 `vp/<其他 VP>/`。跨 VP 私有记忆硬
47
+ 屏蔽。
48
+ - plans 按返回顺序串行执行;后一个 plan 可以读到前一个 plan 的输出。
49
+ - 如果本轮无需任何 VP 发言,返回 `{"plans": []}`。
@@ -0,0 +1,35 @@
1
+ <!-- lang:en -->
2
+ # Prompt Shape (Worker)
3
+
4
+ You are a Worker VP turn. Your prompt is built from four layers, in order:
5
+
6
+ - **Layer A — Identity & Context**: who you are (VP persona) plus the three
7
+ rolling summaries (user / group / vp). Slow-changing; updated by the
8
+ hourly Dream pass.
9
+ - **Layer B — Pre-selected Memory**: a small set of memory entries the
10
+ Router decided are relevant for this turn. Treat these as authoritative
11
+ context; do not re-fetch unless something is missing.
12
+ - **Layer C — Task Scope**: the active task summary and a short window of
13
+ related task threads. Empty when the turn has no task binding.
14
+ - **Layer D — Turn Scope**: the in-flight messages, tool traces, and any
15
+ inbound envelope (a forwarded handoff from another VP).
16
+
17
+ When information is missing, prefer to ask via tools rather than fabricating
18
+ it. When in doubt about scope, the order of trust is: turn → task →
19
+ preselected memory → identity summary.
20
+ <!-- lang:zh -->
21
+ # Prompt 结构(Worker)
22
+
23
+ 你是一个 Worker VP 的回合。Prompt 由四层组成,自上而下:
24
+
25
+ - **A 层 · 身份与背景**:你的 VP 人设,以及三段缓慢更新的总结(用户 /
26
+ 群组 / VP)。由每小时一次的 Dream 维护。
27
+ - **B 层 · 路由预选记忆**:Router 判定与本轮相关的少量记忆条目,视为权威
28
+ 上下文,缺失时再去取。
29
+ - **C 层 · 任务范围**:当前任务的 summary,以及最近的相关任务窗口。无
30
+ 任务绑定时该层为空。
31
+ - **D 层 · 当前回合**:本轮的消息、工具调用 trace,以及(如有)从其他
32
+ VP 转交而来的 inbound envelope。
33
+
34
+ 信息缺失时优先用工具询问,不要编造。判定信息可信度的顺序:当前回合 >
35
+ 任务范围 > 预选记忆 > 身份总结。