@cartanova/qgrid-cli 2.1.0 → 2.2.0

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.
Files changed (37) hide show
  1. package/bundle/dist/application/qgrid/conv-routing.js +68 -0
  2. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -2
  3. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +16 -15
  4. package/bundle/dist/application/qgrid/qgrid.frame.js +17 -5
  5. package/bundle/dist/application/qgrid/qgrid.types.js +12 -3
  6. package/bundle/dist/application/qgrid/tool-emulation.js +11 -6
  7. package/bundle/dist/utils/providers/openai/codex-worker.js +140 -53
  8. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +75 -21
  9. package/bundle/src/application/qgrid/conv-routing.ts +97 -0
  10. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +2 -1
  11. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +16 -5
  12. package/bundle/src/application/qgrid/qgrid.frame.ts +20 -5
  13. package/bundle/src/application/qgrid/qgrid.types.ts +14 -1
  14. package/bundle/src/application/qgrid/tool-emulation.ts +20 -5
  15. package/bundle/src/application/sonamu.generated.http +14 -2
  16. package/bundle/src/utils/providers/common/provider-dispatcher.ts +29 -2
  17. package/bundle/src/utils/providers/openai/codex-worker.test.ts +163 -0
  18. package/bundle/src/utils/providers/openai/codex-worker.ts +199 -39
  19. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +59 -0
  20. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +116 -27
  21. package/bundle/web-dist/client/assets/cost-CpjVoAqp.js +1 -0
  22. package/bundle/web-dist/client/assets/{index-ZV-tHpAg.js → index-DeThumA3.js} +2 -2
  23. package/bundle/web-dist/client/assets/logs-CtmOVH58.js +1 -0
  24. package/bundle/web-dist/client/assets/{routes-BMRwPj6F.js → routes-Cfoe9J0I.js} +1 -1
  25. package/bundle/web-dist/client/assets/show-C6OofoAL.js +30 -0
  26. package/bundle/web-dist/client/assets/{tokens-Cg1f0Ga2.js → tokens-BAZq8sTo.js} +1 -1
  27. package/bundle/web-dist/client/index.html +1 -1
  28. package/bundle/web-dist/server/assets/cost-DzjfVtUr.js +24 -0
  29. package/bundle/web-dist/server/assets/{logs-Xt934T5f.js → logs-CHM0fhfO.js} +2 -7
  30. package/bundle/web-dist/server/assets/{routes-B7QYbNIv.js → routes-C9xFMJ_r.js} +1 -1
  31. package/bundle/web-dist/server/assets/{show-Dxz6UJnD.js → show-BZWqH09U.js} +2 -4
  32. package/bundle/web-dist/server/entry-server.generated.js +3 -3
  33. package/package.json +1 -1
  34. package/bundle/web-dist/client/assets/cost-DE_8cLD-.js +0 -1
  35. package/bundle/web-dist/client/assets/logs-HJYkbqom.js +0 -1
  36. package/bundle/web-dist/client/assets/show-DI6iWDzV.js +0 -30
  37. package/bundle/web-dist/server/assets/cost-DdHTzQDR.js +0 -14
@@ -1,7 +1,12 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
2
 
3
3
  import { type JsonValue } from "../../codex-protocol/serde_json/JsonValue";
4
- import { type QgridContent, type QgridTool, type QueryOutput } from "./qgrid.types";
4
+ import {
5
+ type QgridContent,
6
+ type QgridThreadCoord,
7
+ type QgridTool,
8
+ type QueryOutput,
9
+ } from "./qgrid.types";
5
10
 
6
11
  const logger = getLogger(["qgrid", "tool-emulation"]);
7
12
 
@@ -52,12 +57,16 @@ export function buildToolCallSchema(tools: QgridTool[]): JsonValue {
52
57
  }
53
58
 
54
59
  export function applyToolCallEmulation(
55
- result: Omit<QueryOutput, "content" | "finishReason">,
60
+ result: Omit<QueryOutput, "content" | "finishReason" | "runContext">,
56
61
  tools?: QgridTool[],
62
+ threadCoord?: QgridThreadCoord,
57
63
  ): QueryOutput {
64
+ // thread 재사용 좌표를 runContext 로 실어 올린다 (auto 모드는 requestLogId 없이 threadCoord 만).
65
+ const runContext = threadCoord ? { threadCoord } : undefined;
66
+
58
67
  if (!tools?.length) {
59
68
  const content: QgridContent[] = [{ type: "text", text: result.text }];
60
- return { ...result, content, finishReason: "stop" };
69
+ return { ...result, content, finishReason: "stop", runContext };
61
70
  }
62
71
 
63
72
  let parsed: ToolCallResponse;
@@ -65,7 +74,12 @@ export function applyToolCallEmulation(
65
74
  parsed = JSON.parse(result.text) as ToolCallResponse;
66
75
  } catch (e) {
67
76
  logger.warn(`tool-call emulation parse failed, falling back to text: ${(e as Error).message}`);
68
- return { ...result, content: [{ type: "text", text: result.text }], finishReason: "stop" };
77
+ return {
78
+ ...result,
79
+ content: [{ type: "text", text: result.text }],
80
+ finishReason: "stop",
81
+ runContext,
82
+ };
69
83
  }
70
84
 
71
85
  if (parsed.action === "tool_call") {
@@ -82,7 +96,7 @@ export function applyToolCallEmulation(
82
96
  input: toolCall.args,
83
97
  };
84
98
  });
85
- return { ...result, content, finishReason: "tool-calls" };
99
+ return { ...result, content, finishReason: "tool-calls", runContext };
86
100
  }
87
101
 
88
102
  const text = parsed.answer ?? result.text;
@@ -91,5 +105,6 @@ export function applyToolCallEmulation(
91
105
  text,
92
106
  content: [{ type: "text", text }],
93
107
  finishReason: "stop",
108
+ runContext,
94
109
  };
95
110
  }
@@ -227,7 +227,13 @@ Content-Type: application/json
227
227
  "isStep": false,
228
228
  "logMode": "auto",
229
229
  "runContext": {
230
- "requestLogId": 0
230
+ "requestLogId": 0,
231
+ "threadCoord": {
232
+ "workerId": 0,
233
+ "threadId": "THREADID",
234
+ "epoch": 0,
235
+ "systemHash": "SYSTEMHASH"
236
+ }
231
237
  },
232
238
  "toolResults": [
233
239
  {
@@ -268,7 +274,13 @@ Content-Type: application/json
268
274
  "isStep": false,
269
275
  "logMode": "auto",
270
276
  "runContext": {
271
- "requestLogId": 0
277
+ "requestLogId": 0,
278
+ "threadCoord": {
279
+ "workerId": 0,
280
+ "threadId": "THREADID",
281
+ "epoch": 0,
282
+ "systemHash": "SYSTEMHASH"
283
+ }
272
284
  },
273
285
  "toolResults": [
274
286
  {
@@ -8,17 +8,32 @@ import { type JsonValue } from "../../../codex-protocol/serde_json/JsonValue";
8
8
  import { type TokenUsageBreakdown } from "../../../codex-protocol/v2/TokenUsageBreakdown";
9
9
  import { type UserInput } from "../../../codex-protocol/v2/UserInput";
10
10
 
11
+ // thread 재사용 라우팅 좌표. 상위(qgrid.dispatcher)에서 conv 핸들 검증을 통과한 경우에만 전달.
12
+ // dispatcher 는 이 좌표가 가리키는 worker 의 기존 thread 에 turn 만 실행한다.
13
+ export interface ReuseThreadCoord {
14
+ workerId: number;
15
+ threadId: string;
16
+ epoch: number;
17
+ }
18
+
11
19
  export interface GenerateRequest {
12
20
  model: string;
13
- input: Array<UserInput>;
14
21
  systemPrompt?: string;
15
22
  outputSchema?: JsonValue;
16
23
  effort?: string;
17
24
  verbosity?: string;
18
25
  reasoningSummary?: string;
19
26
  serviceTier?: string;
20
- history?: Array<JsonValue>;
21
27
  abortSignal?: AbortSignal;
28
+ // 첫 turn / 재사용 폴백 시 보낼 input — 전체 prompt. 항상 설정.
29
+ coldInput: Array<UserInput>;
30
+ // 첫 turn / 폴백 시 inject 할 전체 history.
31
+ coldHistory?: Array<JsonValue>;
32
+ // 재사용 좌표 + delta input. 둘은 한 쌍 — 검증 통과 시에만 설정한다.
33
+ // dispatcher 가 worker/thread 생존을 재검증해 성공하면 reuseInput(delta)을, 실패하면
34
+ // coldInput + coldHistory 로 폴백한다(전체 history 로 문맥 복구).
35
+ reuse?: ReuseThreadCoord;
36
+ reuseInput?: Array<UserInput>;
22
37
  }
23
38
 
24
39
  export interface GenerateResult {
@@ -27,6 +42,18 @@ export interface GenerateResult {
27
42
  usage: TokenUsageBreakdown;
28
43
  durationMs: number;
29
44
  model: string;
45
+ // 이번 turn 이 사용한 thread 좌표. 상위가 conv 핸들을 발급/갱신하는 데 쓴다.
46
+ threadCoord: ReuseThreadCoord;
47
+ }
48
+
49
+ // 상위(qgrid.dispatcher)로 가는 스트림 콜백. onComplete 는 non-stream 의 GenerateResult 와
50
+ // 동일 shape(tokenName/threadCoord 포함)를 받아, 두 경로가 같은 일급 타입을 공유한다.
51
+ export interface GenerateStreamCallbacks {
52
+ onDelta: (text: string) => void;
53
+ onComplete: (result: GenerateResult) => void;
54
+ onError: (error: Error) => void;
55
+ onThreadId?: (threadId: string) => void;
56
+ onTurnId?: (turnId: string) => void;
30
57
  }
31
58
 
32
59
  export interface ProviderDispatcher {
@@ -0,0 +1,163 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { CodexAppServerWorker, type StreamCallbacks } from "./codex-worker";
4
+
5
+ function createWorkerWithFakeRpc() {
6
+ const worker = new CodexAppServerWorker({
7
+ tokenId: 1,
8
+ tokenName: "token",
9
+ accessToken: "access",
10
+ accountId: "account",
11
+ });
12
+ const handlers = new Map<string, (params: never) => void>();
13
+ const rpc = {
14
+ onNotification(method: string, handler: (params: never) => void) {
15
+ handlers.set(method, handler);
16
+ },
17
+ };
18
+ (
19
+ worker as unknown as {
20
+ rpc: typeof rpc;
21
+ }
22
+ ).rpc = rpc;
23
+ return { worker, handlers };
24
+ }
25
+
26
+ function createWorkerWithRequestSpy() {
27
+ const worker = new CodexAppServerWorker({
28
+ tokenId: 1,
29
+ tokenName: "token",
30
+ accessToken: "access",
31
+ accountId: "account",
32
+ });
33
+ const request = vi.fn(async (method: string, _params?: unknown) => {
34
+ if (method === "thread/start") {
35
+ return { thread: { id: "thread-1" }, model: "gpt-test" };
36
+ }
37
+ if (method === "turn/start") {
38
+ return { turn: { id: "turn-1" } };
39
+ }
40
+ throw new Error(`unexpected request: ${method}`);
41
+ });
42
+ (
43
+ worker as unknown as {
44
+ rpc: { request: typeof request };
45
+ ready: boolean;
46
+ }
47
+ ).rpc = { request };
48
+ (
49
+ worker as unknown as {
50
+ ready: boolean;
51
+ }
52
+ ).ready = true;
53
+ return { worker, request };
54
+ }
55
+
56
+ describe("CodexAppServerWorker active turn cleanup", () => {
57
+ it("rejects a non-streaming turn when the worker process exits", async () => {
58
+ const { worker } = createWorkerWithFakeRpc();
59
+ const promise = (
60
+ worker as unknown as {
61
+ consumeTurnNotifications(threadId: string, model: string): Promise<unknown>;
62
+ }
63
+ ).consumeTurnNotifications("thread-1", "gpt-test");
64
+
65
+ (
66
+ worker as unknown as {
67
+ failActiveTurn(error: Error): void;
68
+ }
69
+ ).failActiveTurn(new Error("codex worker exited while turn was running"));
70
+
71
+ await expect(promise).rejects.toThrow("codex worker exited while turn was running");
72
+ });
73
+
74
+ it("notifies stream callbacks when the worker process exits", async () => {
75
+ const { worker } = createWorkerWithFakeRpc();
76
+ const callbacks: StreamCallbacks = {
77
+ onDelta: vi.fn(),
78
+ onComplete: vi.fn(),
79
+ onError: vi.fn(),
80
+ };
81
+ const promise = (
82
+ worker as unknown as {
83
+ consumeStreamNotifications(
84
+ threadId: string,
85
+ model: string,
86
+ cb: StreamCallbacks,
87
+ ): Promise<void>;
88
+ }
89
+ ).consumeStreamNotifications("thread-1", "gpt-test", callbacks);
90
+
91
+ (
92
+ worker as unknown as {
93
+ failActiveTurn(error: Error): void;
94
+ }
95
+ ).failActiveTurn(new Error("codex worker exited while turn was running"));
96
+
97
+ await expect(promise).rejects.toThrow("codex worker exited while turn was running");
98
+ expect(callbacks.onError).toHaveBeenCalledWith(
99
+ expect.objectContaining({ message: "codex worker exited while turn was running" }),
100
+ );
101
+ });
102
+ });
103
+
104
+ describe("CodexAppServerWorker prompt prefix", () => {
105
+ it("uses the same base instructions with and without an output schema", async () => {
106
+ type WorkerInternals = {
107
+ createThread(req: unknown): Promise<{ threadId: string; model: string }>;
108
+ startTurnOnThread(threadId: string, req: unknown): Promise<{ turnId: string }>;
109
+ };
110
+
111
+ const textWorker = createWorkerWithRequestSpy();
112
+ {
113
+ const w = textWorker.worker as unknown as WorkerInternals;
114
+ const req = {
115
+ input: [{ type: "text", text: "hello", text_elements: [] }],
116
+ developerInstructions: "fixed system prompt",
117
+ };
118
+ const { threadId } = await w.createThread(req);
119
+ await w.startTurnOnThread(threadId, req);
120
+ }
121
+
122
+ const schemaWorker = createWorkerWithRequestSpy();
123
+ const outputSchema = {
124
+ type: "object",
125
+ additionalProperties: false,
126
+ properties: { answer: { type: "string" } },
127
+ required: ["answer"],
128
+ };
129
+ {
130
+ const w = schemaWorker.worker as unknown as WorkerInternals;
131
+ const req = {
132
+ input: [{ type: "text", text: "hello", text_elements: [] }],
133
+ developerInstructions: "fixed system prompt",
134
+ outputSchema,
135
+ };
136
+ const { threadId } = await w.createThread(req);
137
+ await w.startTurnOnThread(threadId, req);
138
+ }
139
+
140
+ const textThreadStart = textWorker.request.mock.calls.find(
141
+ ([method]) => method === "thread/start",
142
+ )?.[1] as { baseInstructions?: string; developerInstructions?: string } | undefined;
143
+ const schemaThreadStart = schemaWorker.request.mock.calls.find(
144
+ ([method]) => method === "thread/start",
145
+ )?.[1] as { baseInstructions?: string; developerInstructions?: string } | undefined;
146
+ expect(textThreadStart).toEqual(
147
+ expect.objectContaining({
148
+ baseInstructions: schemaThreadStart?.baseInstructions,
149
+ developerInstructions: "fixed system prompt",
150
+ }),
151
+ );
152
+ expect(schemaThreadStart).toEqual(
153
+ expect.objectContaining({
154
+ developerInstructions: "fixed system prompt",
155
+ }),
156
+ );
157
+
158
+ const schemaTurnStart = schemaWorker.request.mock.calls.find(
159
+ ([method]) => method === "turn/start",
160
+ )?.[1];
161
+ expect(schemaTurnStart).toEqual(expect.objectContaining({ outputSchema }));
162
+ });
163
+ });