@hobin/developer 0.1.9 → 0.1.10

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.
@@ -0,0 +1,303 @@
1
+ export const COMPACTION_LANGUAGE_PROTOCOL =
2
+ "developer.compaction-language/v1" as const;
3
+ export const COMPACTION_LANGUAGE_ENTRY =
4
+ "developer.compaction-language" as const;
5
+ export const COMPACTION_LANGUAGE_MESSAGE =
6
+ "developer.compaction-language-continuity" as const;
7
+
8
+ export type LanguageTag = string;
9
+
10
+ export interface LanguageObserved {
11
+ protocol: typeof COMPACTION_LANGUAGE_PROTOCOL;
12
+ kind: "language-observed";
13
+ tag: LanguageTag;
14
+ }
15
+
16
+ export interface ContinuityPending {
17
+ protocol: typeof COMPACTION_LANGUAGE_PROTOCOL;
18
+ kind: "continuity-pending";
19
+ compactionId: string;
20
+ tag: LanguageTag;
21
+ }
22
+
23
+ export interface ContinuityConsumed {
24
+ protocol: typeof COMPACTION_LANGUAGE_PROTOCOL;
25
+ kind: "continuity-consumed";
26
+ compactionId: string;
27
+ }
28
+
29
+ export type CompactionLanguageEvent =
30
+ | LanguageObserved
31
+ | ContinuityPending
32
+ | ContinuityConsumed;
33
+
34
+ export interface PendingContinuity {
35
+ compactionId: string;
36
+ tag: LanguageTag;
37
+ injected: boolean;
38
+ }
39
+
40
+ export interface CompactionLanguageState {
41
+ language?: LanguageTag;
42
+ pending?: PendingContinuity;
43
+ consumedCompactionIds: ReadonlySet<string>;
44
+ }
45
+
46
+ export interface ContinuityContextMessage {
47
+ role: "custom";
48
+ customType: typeof COMPACTION_LANGUAGE_MESSAGE;
49
+ content: Array<{ type: "text"; text: string }>;
50
+ display: false;
51
+ details: {
52
+ protocol: typeof COMPACTION_LANGUAGE_PROTOCOL;
53
+ compactionId: string;
54
+ tag: LanguageTag;
55
+ };
56
+ timestamp: number;
57
+ }
58
+
59
+ interface BranchEntryLike {
60
+ type?: string;
61
+ customType?: string;
62
+ data?: unknown;
63
+ }
64
+
65
+ const KOREAN_SCRIPT = /[\u3131-\u318e\uac00-\ud7a3]/u;
66
+ const ENGLISH_WORD = /[A-Za-z]+(?:'[A-Za-z]+)?/g;
67
+ const COMMAND_ONLY =
68
+ /^(?:npm|pnpm|yarn|node|npx|git|rg|grep|find|cd|ls|cat|sed|bash)\b(?:\s+[-\w./:@]+)*$/u;
69
+ const WEAK_ENGLISH_INPUTS = new Set([
70
+ "continue",
71
+ "done",
72
+ "go on",
73
+ "next",
74
+ "no",
75
+ "ok",
76
+ "okay",
77
+ "proceed",
78
+ "retry",
79
+ "yes",
80
+ ]);
81
+
82
+ function isObject(value: unknown): value is Record<string, unknown> {
83
+ return typeof value === "object" && value !== null;
84
+ }
85
+
86
+ function proseCandidate(text: string): string {
87
+ return text
88
+ .replace(/```[\s\S]*?```/gu, " ")
89
+ .replace(/`[^`\n]*`/gu, " ")
90
+ .replace(/https?:\/\/\S+/gu, " ")
91
+ .replace(/(?:^|\s)(?:\.{0,2}\/|\/)\S+/gu, " ")
92
+ .replace(/\b[\w.-]+\.[A-Za-z0-9]{1,8}\b/gu, " ")
93
+ .replace(/\b(?:route:\S+|[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)\b/gu, " ")
94
+ .replace(/\s+/gu, " ")
95
+ .trim();
96
+ }
97
+
98
+ export function canonicalLanguageTag(value: string): LanguageTag | undefined {
99
+ try {
100
+ return Intl.getCanonicalLocales(value.trim())[0];
101
+ } catch {
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ /** Conservatively observes direct user prose; weak input preserves prior state. */
107
+ export function detectStrongUserLanguage(
108
+ text: string,
109
+ source: string,
110
+ ): LanguageTag | undefined {
111
+ if (source !== "interactive" && source !== "rpc") return undefined;
112
+ const normalized = text.trim();
113
+ if (!normalized) return undefined;
114
+ if (KOREAN_SCRIPT.test(normalized)) return "ko";
115
+
116
+ const lower = normalized.toLowerCase().replace(/\s+/gu, " ");
117
+ if (WEAK_ENGLISH_INPUTS.has(lower) || COMMAND_ONLY.test(normalized)) {
118
+ return undefined;
119
+ }
120
+ const words = proseCandidate(normalized).match(ENGLISH_WORD) ?? [];
121
+ const letterCount = words.reduce((count, word) => count + word.length, 0);
122
+ return words.length >= 2 && letterCount >= 8 ? "en" : undefined;
123
+ }
124
+
125
+ export function languageObserved(tag: LanguageTag): LanguageObserved {
126
+ const canonical = canonicalLanguageTag(tag);
127
+ if (!canonical) throw new Error(`Invalid language tag: ${tag}`);
128
+ return {
129
+ protocol: COMPACTION_LANGUAGE_PROTOCOL,
130
+ kind: "language-observed",
131
+ tag: canonical,
132
+ };
133
+ }
134
+
135
+ export function continuityPending(
136
+ compactionId: string,
137
+ tag: LanguageTag,
138
+ ): ContinuityPending {
139
+ const canonical = canonicalLanguageTag(tag);
140
+ if (!canonical) throw new Error(`Invalid language tag: ${tag}`);
141
+ if (!compactionId.trim()) throw new Error("Compaction ID must not be empty.");
142
+ return {
143
+ protocol: COMPACTION_LANGUAGE_PROTOCOL,
144
+ kind: "continuity-pending",
145
+ compactionId,
146
+ tag: canonical,
147
+ };
148
+ }
149
+
150
+ export function continuityConsumed(compactionId: string): ContinuityConsumed {
151
+ if (!compactionId.trim()) throw new Error("Compaction ID must not be empty.");
152
+ return {
153
+ protocol: COMPACTION_LANGUAGE_PROTOCOL,
154
+ kind: "continuity-consumed",
155
+ compactionId,
156
+ };
157
+ }
158
+
159
+ export function normalizeCompactionLanguageEvent(
160
+ value: unknown,
161
+ ): CompactionLanguageEvent | undefined {
162
+ if (!isObject(value) || value.protocol !== COMPACTION_LANGUAGE_PROTOCOL) {
163
+ return undefined;
164
+ }
165
+ if (value.kind === "language-observed" && typeof value.tag === "string") {
166
+ const tag = canonicalLanguageTag(value.tag);
167
+ return tag ? languageObserved(tag) : undefined;
168
+ }
169
+ if (
170
+ value.kind === "continuity-pending" &&
171
+ typeof value.compactionId === "string" &&
172
+ value.compactionId.trim() &&
173
+ typeof value.tag === "string"
174
+ ) {
175
+ const tag = canonicalLanguageTag(value.tag);
176
+ return tag ? continuityPending(value.compactionId, tag) : undefined;
177
+ }
178
+ if (
179
+ value.kind === "continuity-consumed" &&
180
+ typeof value.compactionId === "string" &&
181
+ value.compactionId.trim()
182
+ ) {
183
+ return continuityConsumed(value.compactionId);
184
+ }
185
+ return undefined;
186
+ }
187
+
188
+ export function initialCompactionLanguageState(): CompactionLanguageState {
189
+ return { consumedCompactionIds: new Set() };
190
+ }
191
+
192
+ export function applyCompactionLanguageEvent(
193
+ state: CompactionLanguageState,
194
+ event: CompactionLanguageEvent,
195
+ ): CompactionLanguageState {
196
+ switch (event.kind) {
197
+ case "language-observed":
198
+ return {
199
+ ...state,
200
+ language: event.tag,
201
+ pending: state.pending
202
+ ? { ...state.pending, tag: event.tag }
203
+ : undefined,
204
+ };
205
+ case "continuity-pending":
206
+ if (
207
+ state.consumedCompactionIds.has(event.compactionId) ||
208
+ state.pending?.compactionId === event.compactionId
209
+ ) {
210
+ return state;
211
+ }
212
+ return {
213
+ ...state,
214
+ pending: {
215
+ compactionId: event.compactionId,
216
+ tag: event.tag,
217
+ injected: false,
218
+ },
219
+ };
220
+ case "continuity-consumed": {
221
+ if (state.consumedCompactionIds.has(event.compactionId)) return state;
222
+ const consumedCompactionIds = new Set(state.consumedCompactionIds);
223
+ consumedCompactionIds.add(event.compactionId);
224
+ return {
225
+ ...state,
226
+ pending:
227
+ state.pending?.compactionId === event.compactionId
228
+ ? undefined
229
+ : state.pending,
230
+ consumedCompactionIds,
231
+ };
232
+ }
233
+ }
234
+ }
235
+
236
+ export function markContinuityInjected(
237
+ state: CompactionLanguageState,
238
+ ): CompactionLanguageState {
239
+ if (!state.pending || state.pending.injected) return state;
240
+ return { ...state, pending: { ...state.pending, injected: true } };
241
+ }
242
+
243
+ export function settlementContinuityEvent(
244
+ state: CompactionLanguageState,
245
+ ): ContinuityConsumed | undefined {
246
+ return state.pending?.injected
247
+ ? continuityConsumed(state.pending.compactionId)
248
+ : undefined;
249
+ }
250
+
251
+ export function reconstructCompactionLanguage(
252
+ entries: ReadonlyArray<BranchEntryLike>,
253
+ ): CompactionLanguageState {
254
+ return entries.reduce((state, entry) => {
255
+ if (
256
+ entry.type !== "custom" ||
257
+ entry.customType !== COMPACTION_LANGUAGE_ENTRY
258
+ ) {
259
+ return state;
260
+ }
261
+ const event = normalizeCompactionLanguageEvent(entry.data);
262
+ return event ? applyCompactionLanguageEvent(state, event) : state;
263
+ }, initialCompactionLanguageState());
264
+ }
265
+
266
+ export function continuityMarkerText(tag: LanguageTag): string {
267
+ return `Post-compaction continuity: user-visible prose language=${tag}; explicit language requests win.`;
268
+ }
269
+
270
+ export function continuityContextMessage(
271
+ pending: PendingContinuity,
272
+ timestamp = Date.now(),
273
+ ): ContinuityContextMessage {
274
+ return {
275
+ role: "custom",
276
+ customType: COMPACTION_LANGUAGE_MESSAGE,
277
+ content: [{ type: "text", text: continuityMarkerText(pending.tag) }],
278
+ display: false,
279
+ details: {
280
+ protocol: COMPACTION_LANGUAGE_PROTOCOL,
281
+ compactionId: pending.compactionId,
282
+ tag: pending.tag,
283
+ },
284
+ timestamp,
285
+ };
286
+ }
287
+
288
+ export function projectCompactionContinuity<T>(
289
+ messages: ReadonlyArray<T>,
290
+ state: CompactionLanguageState,
291
+ timestamp = Date.now(),
292
+ ):
293
+ | {
294
+ messages: Array<T | ContinuityContextMessage>;
295
+ state: CompactionLanguageState;
296
+ }
297
+ | undefined {
298
+ if (!state.pending) return undefined;
299
+ return {
300
+ messages: [...messages, continuityContextMessage(state.pending, timestamp)],
301
+ state: markContinuityInjected(state),
302
+ };
303
+ }
@@ -18,6 +18,19 @@ import {
18
18
  import { Container, Markdown, Text } from "@earendil-works/pi-tui";
19
19
  import { Type, type Static } from "typebox";
20
20
 
21
+ import {
22
+ COMPACTION_LANGUAGE_ENTRY,
23
+ applyCompactionLanguageEvent,
24
+ continuityConsumed,
25
+ continuityPending,
26
+ detectStrongUserLanguage,
27
+ initialCompactionLanguageState,
28
+ languageObserved,
29
+ projectCompactionContinuity,
30
+ reconstructCompactionLanguage,
31
+ settlementContinuityEvent,
32
+ type CompactionLanguageState,
33
+ } from "./compaction-language.ts";
21
34
  import {
22
35
  availablePackageSkills,
23
36
  isWithinRoot,
@@ -621,6 +634,8 @@ function expandedJudgment(
621
634
  export default async function developer(pi: ExtensionAPI) {
622
635
  let availableSkills = new Map<string, Skill>();
623
636
  let state = initialState();
637
+ let compactionLanguage: CompactionLanguageState =
638
+ initialCompactionLanguageState();
624
639
  let routeOpening = false;
625
640
  const routesWithMutation = new Set<string>();
626
641
  let toolPolicyMemory: ToolPolicyMemory = { withheldBuiltins: new Set() };
@@ -732,9 +747,11 @@ export default async function developer(pi: ExtensionAPI) {
732
747
  };
733
748
 
734
749
  const reconstruct = (ctx: ExtensionContext) => {
750
+ const branch = ctx.sessionManager.getBranch();
735
751
  state = toolPolicyRestartRequired
736
752
  ? initialState()
737
- : reconstructState(ctx.sessionManager.getBranch());
753
+ : reconstructState(branch);
754
+ compactionLanguage = reconstructCompactionLanguage(branch);
738
755
  syncProtocolTools();
739
756
  refreshUI(ctx);
740
757
  };
@@ -752,6 +769,16 @@ export default async function developer(pi: ExtensionAPI) {
752
769
  };
753
770
  pi.appendEntry(ACTIVATION_ENTRY, event);
754
771
  state = applyDeveloperEvent(state, event);
772
+ if (!enabled && compactionLanguage.pending) {
773
+ const consumed = continuityConsumed(
774
+ compactionLanguage.pending.compactionId,
775
+ );
776
+ pi.appendEntry(COMPACTION_LANGUAGE_ENTRY, consumed);
777
+ compactionLanguage = applyCompactionLanguageEvent(
778
+ compactionLanguage,
779
+ consumed,
780
+ );
781
+ }
755
782
  syncProtocolTools();
756
783
  refreshUI(ctx);
757
784
  return true;
@@ -2274,6 +2301,41 @@ export default async function developer(pi: ExtensionAPI) {
2274
2301
  },
2275
2302
  );
2276
2303
 
2304
+ pi.on("input", (event) => {
2305
+ if (!state.enabled) return;
2306
+ const tag = detectStrongUserLanguage(event.text, event.source);
2307
+ if (!tag || tag === compactionLanguage.language) return;
2308
+ const observed = languageObserved(tag);
2309
+ pi.appendEntry(COMPACTION_LANGUAGE_ENTRY, observed);
2310
+ compactionLanguage = applyCompactionLanguageEvent(
2311
+ compactionLanguage,
2312
+ observed,
2313
+ );
2314
+ });
2315
+
2316
+ pi.on("session_compact", (event) => {
2317
+ if (!state.enabled || !compactionLanguage.language) return;
2318
+ const pending = continuityPending(
2319
+ event.compactionEntry.id,
2320
+ compactionLanguage.language,
2321
+ );
2322
+ const next = applyCompactionLanguageEvent(compactionLanguage, pending);
2323
+ if (next === compactionLanguage) return;
2324
+ pi.appendEntry(COMPACTION_LANGUAGE_ENTRY, pending);
2325
+ compactionLanguage = next;
2326
+ });
2327
+
2328
+ pi.on("context", (event) => {
2329
+ if (!state.enabled) return;
2330
+ const projection = projectCompactionContinuity(
2331
+ event.messages,
2332
+ compactionLanguage,
2333
+ );
2334
+ if (!projection) return;
2335
+ compactionLanguage = projection.state;
2336
+ return { messages: projection.messages as typeof event.messages };
2337
+ });
2338
+
2277
2339
  pi.on("before_agent_start", (event) => {
2278
2340
  availableSkills = availablePackageSkills(
2279
2341
  event.systemPromptOptions.skills ?? [],
@@ -2373,7 +2435,17 @@ export default async function developer(pi: ExtensionAPI) {
2373
2435
  if (startEnabled === true && !state.enabled) setEnabled(true, ctx);
2374
2436
  });
2375
2437
  pi.on("session_tree", (_event, ctx) => reconstruct(ctx));
2376
- pi.on("agent_settled", (_event, ctx) => refreshUI(ctx));
2438
+ pi.on("agent_settled", (_event, ctx) => {
2439
+ const consumed = settlementContinuityEvent(compactionLanguage);
2440
+ if (consumed) {
2441
+ pi.appendEntry(COMPACTION_LANGUAGE_ENTRY, consumed);
2442
+ compactionLanguage = applyCompactionLanguageEvent(
2443
+ compactionLanguage,
2444
+ consumed,
2445
+ );
2446
+ }
2447
+ refreshUI(ctx);
2448
+ });
2377
2449
  pi.on("session_shutdown", (_event, ctx) => {
2378
2450
  releaseProtocolTools();
2379
2451
  if (!toolPolicyRestartRequired) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hobin/developer",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Branch-aware judgment routing, evidence gates, and focused development skills for Pi.",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package", "pi", "developer", "skills"],