dsh-model-jury 0.1.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 (43) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/COMPATIBILITY.md +17 -0
  3. package/CONTRIBUTING.md +31 -0
  4. package/DEMO.md +64 -0
  5. package/LICENSE +21 -0
  6. package/PRE_FLIGHT.md +39 -0
  7. package/README.md +196 -0
  8. package/REAL_DEMO.md +78 -0
  9. package/SECURITY.md +9 -0
  10. package/cordis.patch.yml +30 -0
  11. package/lib/command.d.ts +13 -0
  12. package/lib/command.js +45 -0
  13. package/lib/config.d.ts +35 -0
  14. package/lib/config.js +39 -0
  15. package/lib/council-service.d.ts +33 -0
  16. package/lib/council-service.js +86 -0
  17. package/lib/index.d.ts +13 -0
  18. package/lib/index.js +12 -0
  19. package/lib/persistence/run-store.d.ts +15 -0
  20. package/lib/persistence/run-store.js +55 -0
  21. package/lib/protocol/aggregate.d.ts +40 -0
  22. package/lib/protocol/aggregate.js +87 -0
  23. package/lib/protocol/anonymize.d.ts +20 -0
  24. package/lib/protocol/anonymize.js +60 -0
  25. package/lib/protocol/prompts.d.ts +12 -0
  26. package/lib/protocol/prompts.js +102 -0
  27. package/lib/protocol/report.d.ts +4 -0
  28. package/lib/protocol/report.js +101 -0
  29. package/lib/protocol/schemas.d.ts +66 -0
  30. package/lib/protocol/schemas.js +48 -0
  31. package/lib/protocol/state-machine.d.ts +41 -0
  32. package/lib/protocol/state-machine.js +109 -0
  33. package/lib/seats/codex-seat.d.ts +17 -0
  34. package/lib/seats/codex-seat.js +81 -0
  35. package/lib/seats/llm-seat.d.ts +17 -0
  36. package/lib/seats/llm-seat.js +96 -0
  37. package/lib/seats/structured.d.ts +22 -0
  38. package/lib/seats/structured.js +126 -0
  39. package/lib/seats/types.d.ts +58 -0
  40. package/lib/seats/types.js +16 -0
  41. package/lib/seats/unavailable-seat.d.ts +13 -0
  42. package/lib/seats/unavailable-seat.js +40 -0
  43. package/package.json +80 -0
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ /** Validated blind-position output. */
3
+ export declare const Round1Schema: z.ZodObject<{
4
+ thesis: z.ZodString;
5
+ recommendation: z.ZodString;
6
+ reasoning: z.ZodArray<z.ZodString>;
7
+ assumptions: z.ZodArray<z.ZodString>;
8
+ risks: z.ZodArray<z.ZodString>;
9
+ evidence_needed: z.ZodArray<z.ZodString>;
10
+ confidence: z.ZodNumber;
11
+ }, z.core.$strip>;
12
+ /** Validated anonymous cross-critique output. */
13
+ export declare const Round2Schema: z.ZodObject<{
14
+ critiques: z.ZodArray<z.ZodObject<{
15
+ target: z.ZodEnum<{
16
+ P1: "P1";
17
+ P2: "P2";
18
+ P3: "P3";
19
+ }>;
20
+ strongest_point: z.ZodString;
21
+ weakest_point: z.ZodString;
22
+ possible_error: z.ZodString;
23
+ missing_evidence: z.ZodString;
24
+ severity: z.ZodEnum<{
25
+ low: "low";
26
+ medium: "medium";
27
+ high: "high";
28
+ critical: "critical";
29
+ }>;
30
+ }, z.core.$strip>>;
31
+ preferred_position: z.ZodEnum<{
32
+ P1: "P1";
33
+ P2: "P2";
34
+ P3: "P3";
35
+ hybrid: "hybrid";
36
+ undecided: "undecided";
37
+ }>;
38
+ reason: z.ZodString;
39
+ confidence: z.ZodNumber;
40
+ }, z.core.$strip>;
41
+ /** Validated final revision output. */
42
+ export declare const Round3Schema: z.ZodObject<{
43
+ final_recommendation: z.ZodString;
44
+ preferred_position: z.ZodEnum<{
45
+ P1: "P1";
46
+ P2: "P2";
47
+ P3: "P3";
48
+ hybrid: "hybrid";
49
+ undecided: "undecided";
50
+ }>;
51
+ changed_mind: z.ZodBoolean;
52
+ change_reason: z.ZodString;
53
+ agreement_points: z.ZodArray<z.ZodString>;
54
+ disagreement_points: z.ZodArray<z.ZodString>;
55
+ critical_risk: z.ZodObject<{
56
+ active: z.ZodBoolean;
57
+ category: z.ZodNullable<z.ZodString>;
58
+ reason: z.ZodNullable<z.ZodString>;
59
+ }, z.core.$strip>;
60
+ confidence: z.ZodNumber;
61
+ }, z.core.$strip>;
62
+ export type Round1Response = z.infer<typeof Round1Schema>;
63
+ export type Round2Response = z.infer<typeof Round2Schema>;
64
+ export type Round3Response = z.infer<typeof Round3Schema>;
65
+ export type PreferredPosition = Round3Response['preferred_position'];
66
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ const confidence = z.number().int().min(0).max(100);
3
+ const nonEmpty = z.string().min(1);
4
+ const preferredPosition = z.enum(['P1', 'P2', 'P3', 'hybrid', 'undecided']);
5
+ /** Validated blind-position output. */
6
+ export const Round1Schema = z.object({
7
+ thesis: nonEmpty,
8
+ recommendation: nonEmpty,
9
+ reasoning: z.array(nonEmpty),
10
+ assumptions: z.array(nonEmpty),
11
+ risks: z.array(nonEmpty),
12
+ evidence_needed: z.array(nonEmpty),
13
+ confidence,
14
+ });
15
+ /** Validated anonymous cross-critique output. */
16
+ export const Round2Schema = z.object({
17
+ critiques: z.array(z.object({
18
+ target: z.enum(['P1', 'P2', 'P3']),
19
+ strongest_point: nonEmpty,
20
+ weakest_point: nonEmpty,
21
+ possible_error: nonEmpty,
22
+ missing_evidence: nonEmpty,
23
+ severity: z.enum(['low', 'medium', 'high', 'critical']),
24
+ })),
25
+ preferred_position: preferredPosition,
26
+ reason: nonEmpty,
27
+ confidence,
28
+ });
29
+ /** Validated final revision output. */
30
+ export const Round3Schema = z.object({
31
+ final_recommendation: nonEmpty,
32
+ preferred_position: preferredPosition,
33
+ changed_mind: z.boolean(),
34
+ change_reason: nonEmpty,
35
+ agreement_points: z.array(nonEmpty),
36
+ disagreement_points: z.array(nonEmpty),
37
+ critical_risk: z.object({
38
+ active: z.boolean(),
39
+ category: z.string().min(1).nullable(),
40
+ reason: z.string().min(1).nullable(),
41
+ }).superRefine((risk, context) => {
42
+ if (risk.active && (risk.category === null || risk.reason === null)) {
43
+ context.addIssue({ code: 'custom', message: 'an active critical risk needs category and reason' });
44
+ }
45
+ }),
46
+ confidence,
47
+ });
48
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1,41 @@
1
+ import type { Agent } from '@deepseek-ai/dsh-agent';
2
+ import type { CouncilSeat, CouncilSeatId, CouncilSeatResponse, SeatCallMetadata } from '../seats/types.js';
3
+ import { type AnonymizationMap, type RedistributionMetadata } from './anonymize.js';
4
+ import { type CouncilFailure, type CouncilVerdict } from './aggregate.js';
5
+ import { type CouncilStyle } from './prompts.js';
6
+ import { type Round1Response, type Round2Response, type Round3Response } from './schemas.js';
7
+ /** Ordered deterministic protocol phases. */
8
+ export type CouncilState = 'idle' | 'round1' | 'anonymized' | 'round2' | 'round3' | 'aggregated' | 'completed';
9
+ /** Observer used by persistence and tests without coupling protocol logic to storage. */
10
+ export interface CouncilRunObserver {
11
+ stateChanged?(state: CouncilState): Promise<void> | void;
12
+ prompt?(round: 'round1' | 'round2' | 'round3', seat: CouncilSeatId, prompt: string): Promise<void> | void;
13
+ response?<T>(round: 'round1' | 'round2' | 'round3', seat: CouncilSeatId, response: CouncilSeatResponse<T>): Promise<void> | void;
14
+ failure?(failure: CouncilFailure, metadata?: SeatCallMetadata): Promise<void> | void;
15
+ redistribution?(metadata: Readonly<Record<string, RedistributionMetadata>>): Promise<void> | void;
16
+ }
17
+ /** Complete structured output of the deterministic protocol. */
18
+ export interface CouncilRunResult {
19
+ readonly question: string;
20
+ readonly style: CouncilStyle;
21
+ readonly mapping: AnonymizationMap;
22
+ readonly round1: ReadonlyMap<CouncilSeatId, CouncilSeatResponse<Round1Response>>;
23
+ readonly round2: ReadonlyMap<CouncilSeatId, CouncilSeatResponse<Round2Response>>;
24
+ readonly round3: ReadonlyMap<CouncilSeatId, CouncilSeatResponse<Round3Response>>;
25
+ readonly failures: readonly CouncilFailure[];
26
+ readonly verdict: CouncilVerdict;
27
+ }
28
+ /** Code-owned three-round Council state machine. */
29
+ export declare class CouncilStateMachine {
30
+ private readonly seats;
31
+ private readonly maxFieldChars;
32
+ private readonly observer;
33
+ private readonly createMapping;
34
+ private state;
35
+ constructor(seats: readonly CouncilSeat[], maxFieldChars: number, observer?: CouncilRunObserver, createMapping?: () => AnonymizationMap);
36
+ private transition;
37
+ private invokeRound;
38
+ /** Run blind positions, anonymous critique, revision, and deterministic aggregation. */
39
+ run(question: string, style: CouncilStyle, parent: Agent, signal: AbortSignal): Promise<CouncilRunResult>;
40
+ }
41
+ //# sourceMappingURL=state-machine.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { SEAT_NAMES, SeatInvocationError } from '../seats/types.js';
2
+ import { createAnonymizationMap, sanitizeForRedistribution, } from './anonymize.js';
3
+ import { aggregateCouncil } from './aggregate.js';
4
+ import { round1Prompt, round2Prompt, round3Prompt } from './prompts.js';
5
+ import { Round1Schema, Round2Schema, Round3Schema, } from './schemas.js';
6
+ function errorText(error) {
7
+ return error instanceof Error ? error.message : String(error);
8
+ }
9
+ function callMetadata(error) {
10
+ return error instanceof SeatInvocationError ? error.metadata : undefined;
11
+ }
12
+ function identityTerms(seats) {
13
+ return [
14
+ 'GPT', 'GLM', 'DeepSeek', 'Codex', 'OpenAI', 'Zhipu', 'ZhipuAI', 'BigModel',
15
+ ...seats.flatMap(seat => [seat.id, SEAT_NAMES[seat.id], seat.provider, seat.model ?? '']),
16
+ ];
17
+ }
18
+ function responseMap(responses) {
19
+ return new Map([...responses].map(([seat, response]) => [seat, response.value]));
20
+ }
21
+ /** Code-owned three-round Council state machine. */
22
+ export class CouncilStateMachine {
23
+ seats;
24
+ maxFieldChars;
25
+ observer;
26
+ createMapping;
27
+ state = 'idle';
28
+ constructor(seats, maxFieldChars, observer = {}, createMapping = createAnonymizationMap) {
29
+ this.seats = seats;
30
+ this.maxFieldChars = maxFieldChars;
31
+ this.observer = observer;
32
+ this.createMapping = createMapping;
33
+ const ids = new Set(seats.map(seat => seat.id));
34
+ if (seats.length !== 3 || ids.size !== 3)
35
+ throw new Error('Council needs exactly one GPT, GLM, and DeepSeek seat');
36
+ }
37
+ async transition(expected, next) {
38
+ if (this.state !== expected)
39
+ throw new Error(`invalid Council transition ${this.state} -> ${next}`);
40
+ this.state = next;
41
+ await this.observer.stateChanged?.(next);
42
+ }
43
+ async invokeRound(round, schemaName, schema, promptFor, parent, signal, failures) {
44
+ const calls = this.seats.map(async (seat) => {
45
+ const prompt = promptFor(seat);
46
+ if (prompt === undefined)
47
+ return;
48
+ await this.observer.prompt?.(round, seat.id, prompt);
49
+ try {
50
+ const response = await seat.invoke({ round, prompt, schema, schemaName }, parent, signal);
51
+ await this.observer.response?.(round, seat.id, response);
52
+ return [seat.id, response];
53
+ }
54
+ catch (error) {
55
+ const failure = { seat: seat.id, round, error: errorText(error) };
56
+ failures.push(failure);
57
+ await this.observer.failure?.(failure, callMetadata(error));
58
+ return undefined;
59
+ }
60
+ });
61
+ const settled = await Promise.all(calls);
62
+ signal.throwIfAborted();
63
+ return new Map(settled.filter((entry) => entry !== undefined));
64
+ }
65
+ /** Run blind positions, anonymous critique, revision, and deterministic aggregation. */
66
+ async run(question, style, parent, signal) {
67
+ const failures = [];
68
+ const identities = identityTerms(this.seats);
69
+ await this.transition('idle', 'round1');
70
+ const firstPrompt = round1Prompt(question);
71
+ const round1 = await this.invokeRound('round1', 'Round1', Round1Schema, () => firstPrompt, parent, signal, failures);
72
+ await this.transition('round1', 'anonymized');
73
+ const mapping = this.createMapping();
74
+ const sanitizedRound1 = new Map();
75
+ const redistribution = {};
76
+ for (const [seat, response] of round1) {
77
+ const sanitized = sanitizeForRedistribution(response.value, this.maxFieldChars, identities);
78
+ sanitizedRound1.set(seat, sanitized.value);
79
+ redistribution[`round1.${seat}`] = sanitized.metadata;
80
+ }
81
+ const positionEntries = [...sanitizedRound1].map(([seat, value]) => [
82
+ mapping[seat],
83
+ value,
84
+ ]);
85
+ positionEntries.sort((left, right) => left[0].localeCompare(right[0]));
86
+ const positions = Object.fromEntries(positionEntries);
87
+ await this.observer.redistribution?.(redistribution);
88
+ await this.transition('anonymized', 'round2');
89
+ const round2 = await this.invokeRound('round2', 'Round2', Round2Schema, seat => sanitizedRound1.has(seat.id)
90
+ ? round2Prompt(question, sanitizedRound1.get(seat.id), positions, style)
91
+ : undefined, parent, signal, failures);
92
+ const sanitizedCritiques = [];
93
+ for (const [seat, response] of round2) {
94
+ const sanitized = sanitizeForRedistribution(response.value, this.maxFieldChars, identities);
95
+ sanitizedCritiques.push(sanitized.value);
96
+ redistribution[`round2.${seat}`] = sanitized.metadata;
97
+ }
98
+ await this.observer.redistribution?.(redistribution);
99
+ await this.transition('round2', 'round3');
100
+ const round3 = await this.invokeRound('round3', 'Round3', Round3Schema, seat => sanitizedRound1.has(seat.id)
101
+ ? round3Prompt(question, sanitizedRound1.get(seat.id), positions, sanitizedCritiques)
102
+ : undefined, parent, signal, failures);
103
+ await this.transition('round3', 'aggregated');
104
+ const verdict = aggregateCouncil(responseMap(round3), responseMap(round1), mapping, failures);
105
+ await this.transition('aggregated', 'completed');
106
+ return { question, style, mapping, round1, round2, round3, failures, verdict };
107
+ }
108
+ }
109
+ //# sourceMappingURL=state-machine.js.map
@@ -0,0 +1,17 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ import type { SeatConnectionConfig } from '../config.js';
4
+ import type { CouncilSeat, CouncilSeatRequest, CouncilSeatResponse, SeatHealth } from './types.js';
5
+ /** GPT seat backed by DSH's first-party Codex subagent provider. */
6
+ export declare class CodexSeat implements CouncilSeat {
7
+ private readonly ctx;
8
+ private readonly config;
9
+ readonly id: "gpt";
10
+ readonly provider: string;
11
+ readonly model: undefined;
12
+ constructor(ctx: Context, config: SeatConnectionConfig);
13
+ private execute;
14
+ invoke<T>(request: CouncilSeatRequest<T>, parent: Agent, signal: AbortSignal): Promise<CouncilSeatResponse<T>>;
15
+ healthCheck(parent: Agent, signal: AbortSignal): Promise<SeatHealth>;
16
+ }
17
+ //# sourceMappingURL=codex-seat.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { invokeStructured } from './structured.js';
2
+ function visibleText(blocks) {
3
+ return blocks
4
+ .filter((block) => block.type === 'text')
5
+ .map(block => block.text)
6
+ .join('')
7
+ .trim();
8
+ }
9
+ /** GPT seat backed by DSH's first-party Codex subagent provider. */
10
+ export class CodexSeat {
11
+ ctx;
12
+ config;
13
+ id = 'gpt';
14
+ provider;
15
+ model = undefined;
16
+ constructor(ctx, config) {
17
+ this.ctx = ctx;
18
+ this.config = config;
19
+ this.provider = config.provider;
20
+ }
21
+ async execute(prompt, parent, signal) {
22
+ const run = await this.ctx.subagents.start(this.provider, {
23
+ label: 'Council GPT seat',
24
+ prompt: [{ type: 'text', text: prompt }],
25
+ parent,
26
+ signal,
27
+ });
28
+ try {
29
+ const result = await run.result;
30
+ const text = visibleText(result.output);
31
+ if (result.stopReason !== 'completed') {
32
+ const error = new Error(result.diagnostic ?? `Codex stopped with ${result.stopReason}`);
33
+ error.code = result.stopReason === 'aborted' ? 'ABORTED' : 'CODEX_ERROR';
34
+ throw error;
35
+ }
36
+ if (text.length === 0)
37
+ throw new Error('Codex produced no visible final answer');
38
+ return { text };
39
+ }
40
+ finally {
41
+ await run.dispose();
42
+ }
43
+ }
44
+ invoke(request, parent, signal) {
45
+ return invokeStructured({
46
+ seat: this.id,
47
+ provider: this.provider,
48
+ model: undefined,
49
+ timeoutMs: this.config.timeoutMs,
50
+ maxRetries: this.config.maxRetries,
51
+ }, request, (prompt, callSignal) => this.execute(prompt, parent, callSignal), signal);
52
+ }
53
+ async healthCheck(parent, signal) {
54
+ const startedAt = Date.now();
55
+ const health = (ok, detail) => ({
56
+ ok,
57
+ detail,
58
+ provider: this.provider,
59
+ model: 'native Codex app-server',
60
+ durationMs: Math.max(0, Date.now() - startedAt),
61
+ });
62
+ if (this.ctx.subagents.getProvider(this.provider) === undefined) {
63
+ return health(false, `subagent provider "${this.provider}" is not registered`);
64
+ }
65
+ try {
66
+ const healthSignal = AbortSignal.any([signal, AbortSignal.timeout(this.config.timeoutMs)]);
67
+ const result = await this.execute([
68
+ 'This is an analytical read-only health check.',
69
+ 'Do not call tools or modify anything.',
70
+ 'Return exactly: CODEX_SUBSCRIPTION_OK',
71
+ ].join('\n'), parent, healthSignal);
72
+ return result.text.trim() === 'CODEX_SUBSCRIPTION_OK'
73
+ ? health(true, 'native Codex authentication completed a real app-server child')
74
+ : health(false, 'Codex returned an unexpected health-check response');
75
+ }
76
+ catch (error) {
77
+ return health(false, error instanceof Error ? error.message : String(error));
78
+ }
79
+ }
80
+ }
81
+ //# sourceMappingURL=codex-seat.js.map
@@ -0,0 +1,17 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ import type { LlmSeatConnectionConfig } from '../config.js';
4
+ import type { CouncilSeat, CouncilSeatId, CouncilSeatRequest, CouncilSeatResponse, SeatHealth } from './types.js';
5
+ /** GLM or DeepSeek seat backed by a configured DSH LLM route. */
6
+ export declare class LlmSeat implements CouncilSeat {
7
+ readonly id: Extract<CouncilSeatId, 'glm' | 'deepseek'>;
8
+ private readonly ctx;
9
+ private readonly config;
10
+ readonly provider: string;
11
+ readonly model: string | undefined;
12
+ constructor(id: Extract<CouncilSeatId, 'glm' | 'deepseek'>, ctx: Context, config: LlmSeatConnectionConfig);
13
+ private execute;
14
+ invoke<T>(request: CouncilSeatRequest<T>, _parent: Agent, signal: AbortSignal): Promise<CouncilSeatResponse<T>>;
15
+ healthCheck(_parent: Agent, signal: AbortSignal): Promise<SeatHealth>;
16
+ }
17
+ //# sourceMappingURL=llm-seat.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { BlockAssembler, createUserMessage, } from '@deepseek-ai/dsh-llm';
2
+ import { invokeStructured } from './structured.js';
3
+ function finishError(finish) {
4
+ if (finish.kind === 'stop')
5
+ return undefined;
6
+ if (finish.kind === 'error' || finish.kind === 'aborted') {
7
+ const error = new Error(finish.failure.message);
8
+ error.code = finish.failure.code;
9
+ return error;
10
+ }
11
+ const error = new Error(`model stopped with ${finish.kind}`);
12
+ error.code = finish.kind === 'max-tokens' ? 'MAX_TOKENS' : 'UNEXPECTED_FINISH';
13
+ return error;
14
+ }
15
+ /** GLM or DeepSeek seat backed by a configured DSH LLM route. */
16
+ export class LlmSeat {
17
+ id;
18
+ ctx;
19
+ config;
20
+ provider;
21
+ model;
22
+ constructor(id, ctx, config) {
23
+ this.id = id;
24
+ this.ctx = ctx;
25
+ this.config = config;
26
+ this.provider = config.provider;
27
+ this.model = config.model ?? undefined;
28
+ }
29
+ async execute(prompt, signal) {
30
+ if (this.model === undefined)
31
+ throw new Error(`${this.id} model is not configured`);
32
+ const message = createUserMessage({
33
+ source: { kind: 'plugin', plugin: 'dsh-model-jury' },
34
+ content: [{ type: 'text', text: prompt }],
35
+ });
36
+ const assembler = new BlockAssembler();
37
+ for await (const chunk of this.ctx.llm.stream({
38
+ provider: this.provider,
39
+ model: this.model,
40
+ messages: [message],
41
+ maxTokens: this.config.maxTokens,
42
+ signal,
43
+ })) {
44
+ signal.throwIfAborted();
45
+ assembler.push(chunk);
46
+ }
47
+ const terminalError = finishError(assembler.finish);
48
+ if (terminalError !== undefined)
49
+ throw terminalError;
50
+ const text = assembler.blocks()
51
+ .filter((block) => block.type === 'text')
52
+ .map(block => block.text)
53
+ .join('')
54
+ .trim();
55
+ if (text.length === 0)
56
+ throw new Error(`${this.id} produced no visible text`);
57
+ return { text, ...assembler.usage === undefined ? {} : { usage: assembler.usage } };
58
+ }
59
+ invoke(request, _parent, signal) {
60
+ return invokeStructured({
61
+ seat: this.id,
62
+ provider: this.provider,
63
+ model: this.model,
64
+ timeoutMs: this.config.timeoutMs,
65
+ maxRetries: this.config.maxRetries,
66
+ }, request, (prompt, callSignal) => this.execute(prompt, callSignal), signal);
67
+ }
68
+ async healthCheck(_parent, signal) {
69
+ const startedAt = Date.now();
70
+ const health = (ok, detail) => ({
71
+ ok,
72
+ detail,
73
+ provider: this.provider,
74
+ ...(this.model === undefined ? {} : { model: this.model }),
75
+ durationMs: Math.max(0, Date.now() - startedAt),
76
+ });
77
+ if (this.model === undefined)
78
+ return health(false, 'model is not configured');
79
+ if (!this.ctx.llm.listProviders().some(provider => provider.id === this.provider)) {
80
+ return health(false, `LLM provider route "${this.provider}" is not registered`);
81
+ }
82
+ try {
83
+ const healthSignal = AbortSignal.any([signal, AbortSignal.timeout(this.config.timeoutMs)]);
84
+ await this.ctx.llm.resolveModelInfo(this.provider, this.model, healthSignal);
85
+ const result = await this.execute('Return exactly this JSON object and nothing else: {"ok":true}', healthSignal);
86
+ const parsed = JSON.parse(result.text);
87
+ return typeof parsed === 'object' && parsed !== null && parsed.ok === true
88
+ ? health(true, `${this.provider}/${this.model} completed a real request`)
89
+ : health(false, 'provider returned an unexpected health-check response');
90
+ }
91
+ catch (error) {
92
+ return health(false, error instanceof Error ? error.message : String(error));
93
+ }
94
+ }
95
+ }
96
+ //# sourceMappingURL=llm-seat.js.map
@@ -0,0 +1,22 @@
1
+ import type { TokenUsage } from '@deepseek-ai/dsh-llm';
2
+ import type { CouncilSeatId, CouncilSeatRequest, CouncilSeatResponse } from './types.js';
3
+ /** Raw visible output from one provider call. */
4
+ export interface RawSeatResult {
5
+ readonly text: string;
6
+ readonly usage?: TokenUsage;
7
+ }
8
+ /** Provider call used by the shared structured-output policy. */
9
+ export type RawSeatExecutor = (prompt: string, signal: AbortSignal) => Promise<RawSeatResult>;
10
+ /** Runtime policy shared by Codex and LLM seats. */
11
+ export interface StructuredInvokePolicy {
12
+ readonly seat: CouncilSeatId;
13
+ readonly provider: string;
14
+ readonly model: string | undefined;
15
+ readonly timeoutMs: number;
16
+ readonly maxRetries: number;
17
+ }
18
+ /** Parse a JSON object from plain or fenced model output. */
19
+ export declare function parseJsonObject(raw: string): unknown;
20
+ /** Execute, validate, and perform at most one structured repair call. */
21
+ export declare function invokeStructured<T>(policy: StructuredInvokePolicy, request: CouncilSeatRequest<T>, execute: RawSeatExecutor, parentSignal: AbortSignal): Promise<CouncilSeatResponse<T>>;
22
+ //# sourceMappingURL=structured.d.ts.map
@@ -0,0 +1,126 @@
1
+ import { repairPrompt } from '../protocol/prompts.js';
2
+ import { SeatInvocationError } from './types.js';
3
+ /** Parse a JSON object from plain or fenced model output. */
4
+ export function parseJsonObject(raw) {
5
+ const trimmed = raw.trim();
6
+ const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/iu.exec(trimmed);
7
+ const candidate = fenced?.[1] ?? trimmed;
8
+ const parsed = JSON.parse(candidate);
9
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
10
+ throw new Error('response root must be a JSON object');
11
+ }
12
+ return parsed;
13
+ }
14
+ function errorText(error) {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ function errorCode(error) {
18
+ if (typeof error !== 'object' || error === null || !('code' in error))
19
+ return undefined;
20
+ return typeof error.code === 'string' ? error.code : undefined;
21
+ }
22
+ function isTransient(error) {
23
+ const code = errorCode(error)?.toUpperCase();
24
+ if (code !== undefined) {
25
+ if (['AUTH', 'MISSING_CREDENTIAL', 'INVALID_CREDENTIAL', 'INVALID_REQUEST'].includes(code))
26
+ return false;
27
+ if (['RATE_LIMIT', 'SERVER', 'TRANSPORT', 'TIMEOUT', 'STREAM_CLOSED', 'EMPTY_RESPONSE'].includes(code))
28
+ return true;
29
+ if (/^HTTP_5\d\d$/u.test(code))
30
+ return true;
31
+ }
32
+ const message = errorText(error).toLowerCase();
33
+ return /rate.?limit|temporar|timed? ?out|connection reset|transport|service unavailable/u.test(message);
34
+ }
35
+ function deadline(parent, timeoutMs) {
36
+ const controller = new AbortController();
37
+ const onAbort = () => controller.abort(parent.reason);
38
+ if (parent.aborted)
39
+ onAbort();
40
+ else
41
+ parent.addEventListener('abort', onAbort, { once: true });
42
+ const timer = setTimeout(() => controller.abort(new Error(`seat timed out after ${timeoutMs}ms`)), timeoutMs);
43
+ return {
44
+ signal: controller.signal,
45
+ dispose() {
46
+ clearTimeout(timer);
47
+ parent.removeEventListener('abort', onAbort);
48
+ },
49
+ };
50
+ }
51
+ async function callWithRetry(execute, prompt, signal, maxRetries, onRetry) {
52
+ let retries = 0;
53
+ while (true) {
54
+ signal.throwIfAborted();
55
+ try {
56
+ return await execute(prompt, signal);
57
+ }
58
+ catch (error) {
59
+ if (signal.aborted || retries >= maxRetries || !isTransient(error))
60
+ throw error;
61
+ retries += 1;
62
+ onRetry();
63
+ }
64
+ }
65
+ }
66
+ function metadata(policy, request, startedAt, success, retryCount, repairCount, usage) {
67
+ const endedAt = new Date();
68
+ return {
69
+ seat: policy.seat,
70
+ provider: policy.provider,
71
+ model: policy.model,
72
+ round: request.round,
73
+ startedAt: startedAt.toISOString(),
74
+ endedAt: endedAt.toISOString(),
75
+ durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime()),
76
+ success,
77
+ retryCount,
78
+ repairCount,
79
+ tokenUsage: usage,
80
+ };
81
+ }
82
+ /** Execute, validate, and perform at most one structured repair call. */
83
+ export async function invokeStructured(policy, request, execute, parentSignal) {
84
+ const startedAt = new Date();
85
+ const callDeadline = deadline(parentSignal, policy.timeoutMs);
86
+ let retries = 0;
87
+ let repairs = 0;
88
+ let latestUsage;
89
+ try {
90
+ let called = await callWithRetry(execute, request.prompt, callDeadline.signal, policy.maxRetries, () => { retries += 1; });
91
+ latestUsage = called.usage;
92
+ let raw = called.text;
93
+ let validation = request.schema.safeParse(parseJsonObjectSafely(raw));
94
+ if (!validation.success) {
95
+ repairs = 1;
96
+ called = await callWithRetry(execute, repairPrompt(request.prompt, raw, validation.error.message), callDeadline.signal, policy.maxRetries, () => { retries += 1; });
97
+ latestUsage = called.usage ?? latestUsage;
98
+ raw = called.text;
99
+ validation = request.schema.safeParse(parseJsonObjectSafely(raw));
100
+ }
101
+ if (!validation.success) {
102
+ throw new Error(`${request.schemaName} response remained malformed after one repair: ${validation.error.message}`);
103
+ }
104
+ return {
105
+ value: validation.data,
106
+ raw,
107
+ metadata: metadata(policy, request, startedAt, true, retries, repairs, latestUsage),
108
+ };
109
+ }
110
+ catch (error) {
111
+ const callMetadata = metadata(policy, request, startedAt, false, retries, repairs, latestUsage);
112
+ throw new SeatInvocationError(`${policy.seat} ${request.round} failed: ${errorText(error)}`, callMetadata, { cause: error });
113
+ }
114
+ finally {
115
+ callDeadline.dispose();
116
+ }
117
+ }
118
+ function parseJsonObjectSafely(raw) {
119
+ try {
120
+ return parseJsonObject(raw);
121
+ }
122
+ catch (error) {
123
+ return { __council_parse_error__: errorText(error) };
124
+ }
125
+ }
126
+ //# sourceMappingURL=structured.js.map