@kweaver-ai/kweaver-sdk 0.7.4 → 0.8.2
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/README.md +39 -5
- package/README.zh.md +37 -5
- package/dist/agent-providers/index.d.ts +7 -0
- package/dist/agent-providers/index.js +5 -0
- package/dist/agent-providers/prompt-template.d.ts +62 -0
- package/dist/agent-providers/prompt-template.js +105 -0
- package/dist/agent-providers/prompts/rubric-judge-v1.prompt.md +51 -0
- package/dist/agent-providers/prompts/within-trace-synthesizer-v1.prompt.md +60 -0
- package/dist/agent-providers/providers/claude-code-subprocess.d.ts +74 -0
- package/dist/agent-providers/providers/claude-code-subprocess.js +259 -0
- package/dist/agent-providers/providers/stub.d.ts +47 -0
- package/dist/agent-providers/providers/stub.js +77 -0
- package/dist/agent-providers/registry.d.ts +45 -0
- package/dist/agent-providers/registry.js +77 -0
- package/dist/agent-providers/types.d.ts +91 -0
- package/dist/agent-providers/types.js +25 -0
- package/dist/api/agent-chat.js +8 -6
- package/dist/api/agent-observability.d.ts +51 -0
- package/dist/api/agent-observability.js +108 -0
- package/dist/api/context-loader.d.ts +1 -0
- package/dist/api/conversations.d.ts +4 -8
- package/dist/api/conversations.js +16 -58
- package/dist/api/datasources.d.ts +2 -20
- package/dist/api/datasources.js +7 -123
- package/dist/api/semantic-search.d.ts +5 -0
- package/dist/api/semantic-search.js +5 -0
- package/dist/api/skills.d.ts +75 -2
- package/dist/api/skills.js +108 -12
- package/dist/api/trace.d.ts +49 -0
- package/dist/api/trace.js +85 -0
- package/dist/api/vega.d.ts +53 -0
- package/dist/api/vega.js +144 -0
- package/dist/cli.js +12 -5
- package/dist/commands/agent/mode.d.ts +6 -0
- package/dist/commands/agent/mode.js +75 -0
- package/dist/commands/agent.js +101 -29
- package/dist/commands/bkn-ops.js +12 -6
- package/dist/commands/bkn-utils.d.ts +9 -0
- package/dist/commands/bkn-utils.js +17 -0
- package/dist/commands/context-loader.js +608 -38
- package/dist/commands/ds.js +7 -2
- package/dist/commands/skill.d.ts +21 -1
- package/dist/commands/skill.js +389 -1
- package/dist/commands/trace.d.ts +39 -0
- package/dist/commands/trace.js +668 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/resources/bkn.d.ts +5 -0
- package/dist/resources/bkn.js +5 -0
- package/dist/resources/datasources.js +2 -1
- package/dist/resources/skills.d.ts +17 -1
- package/dist/resources/skills.js +32 -1
- package/dist/trace-ai/diagnose/agent-binding.d.ts +67 -0
- package/dist/trace-ai/diagnose/agent-binding.js +257 -0
- package/dist/trace-ai/diagnose/builtin-rules/excessive-tool-calls-per-turn.d.ts +2 -0
- package/dist/trace-ai/diagnose/builtin-rules/excessive-tool-calls-per-turn.js +15 -0
- package/dist/trace-ai/diagnose/builtin-rules/excessive-tool-calls-per-turn.yaml +16 -0
- package/dist/trace-ai/diagnose/builtin-rules/llm-response-truncated-no-continue.d.ts +2 -0
- package/dist/trace-ai/diagnose/builtin-rules/llm-response-truncated-no-continue.js +44 -0
- package/dist/trace-ai/diagnose/builtin-rules/llm-response-truncated-no-continue.yaml +15 -0
- package/dist/trace-ai/diagnose/builtin-rules/register.d.ts +1 -0
- package/dist/trace-ai/diagnose/builtin-rules/register.js +11 -0
- package/dist/trace-ai/diagnose/builtin-rules/retrieval-empty-no-fallback.d.ts +2 -0
- package/dist/trace-ai/diagnose/builtin-rules/retrieval-empty-no-fallback.js +29 -0
- package/dist/trace-ai/diagnose/builtin-rules/retrieval-empty-no-fallback.yaml +15 -0
- package/dist/trace-ai/diagnose/builtin-rules/tool-error-swallowed.d.ts +2 -0
- package/dist/trace-ai/diagnose/builtin-rules/tool-error-swallowed.js +45 -0
- package/dist/trace-ai/diagnose/builtin-rules/tool-error-swallowed.yaml +15 -0
- package/dist/trace-ai/diagnose/builtin-rules/tool-loop-no-state-change.d.ts +2 -0
- package/dist/trace-ai/diagnose/builtin-rules/tool-loop-no-state-change.js +38 -0
- package/dist/trace-ai/diagnose/builtin-rules/tool-loop-no-state-change.yaml +16 -0
- package/dist/trace-ai/diagnose/builtin-rules/tool-retry-intent-mismatch.yaml +68 -0
- package/dist/trace-ai/diagnose/index.d.ts +32 -0
- package/dist/trace-ai/diagnose/index.js +246 -0
- package/dist/trace-ai/diagnose/output-schema-converter.d.ts +24 -0
- package/dist/trace-ai/diagnose/output-schema-converter.js +81 -0
- package/dist/trace-ai/diagnose/predicate-registry.d.ts +7 -0
- package/dist/trace-ai/diagnose/predicate-registry.js +30 -0
- package/dist/trace-ai/diagnose/query-extractor.d.ts +14 -0
- package/dist/trace-ai/diagnose/query-extractor.js +45 -0
- package/dist/trace-ai/diagnose/report-assembler.d.ts +31 -0
- package/dist/trace-ai/diagnose/report-assembler.js +100 -0
- package/dist/trace-ai/diagnose/report-markdown.d.ts +18 -0
- package/dist/trace-ai/diagnose/report-markdown.js +192 -0
- package/dist/trace-ai/diagnose/rule-loader.d.ts +11 -0
- package/dist/trace-ai/diagnose/rule-loader.js +120 -0
- package/dist/trace-ai/diagnose/schemas.d.ts +184 -0
- package/dist/trace-ai/diagnose/schemas.js +154 -0
- package/dist/trace-ai/diagnose/signal-probe.d.ts +17 -0
- package/dist/trace-ai/diagnose/signal-probe.js +39 -0
- package/dist/trace-ai/diagnose/synthesizer-agent.d.ts +40 -0
- package/dist/trace-ai/diagnose/synthesizer-agent.js +158 -0
- package/dist/trace-ai/diagnose/synthesizer-template.d.ts +2 -0
- package/dist/trace-ai/diagnose/synthesizer-template.js +49 -0
- package/dist/trace-ai/diagnose/trace-shaper.d.ts +3 -0
- package/dist/trace-ai/diagnose/trace-shaper.js +73 -0
- package/dist/trace-ai/diagnose/types.d.ts +173 -0
- package/dist/trace-ai/diagnose/types.js +1 -0
- package/dist/trace-ai/eval-set/assertion-evaluator.d.ts +29 -0
- package/dist/trace-ai/eval-set/assertion-evaluator.js +100 -0
- package/dist/trace-ai/eval-set/builder.d.ts +36 -0
- package/dist/trace-ai/eval-set/builder.js +126 -0
- package/dist/trace-ai/eval-set/index.d.ts +15 -0
- package/dist/trace-ai/eval-set/index.js +10 -0
- package/dist/trace-ai/eval-set/output-writer.d.ts +27 -0
- package/dist/trace-ai/eval-set/output-writer.js +126 -0
- package/dist/trace-ai/eval-set/query-picker.d.ts +37 -0
- package/dist/trace-ai/eval-set/query-picker.js +147 -0
- package/dist/trace-ai/eval-set/redactor.d.ts +42 -0
- package/dist/trace-ai/eval-set/redactor.js +133 -0
- package/dist/trace-ai/eval-set/rubric-templates/answer-match-reference.prompt.md +19 -0
- package/dist/trace-ai/eval-set/schemas.d.ts +136 -0
- package/dist/trace-ai/eval-set/schemas.js +130 -0
- package/dist/trace-ai/eval-set/semantic-match-provider.d.ts +33 -0
- package/dist/trace-ai/eval-set/semantic-match-provider.js +51 -0
- package/dist/trace-ai/eval-set/test-runner.d.ts +34 -0
- package/dist/trace-ai/eval-set/test-runner.js +153 -0
- package/dist/trace-ai/eval-set/types.d.ts +46 -0
- package/dist/trace-ai/eval-set/types.js +8 -0
- package/dist/trace-ai/exp/bundle-writer.d.ts +10 -0
- package/dist/trace-ai/exp/bundle-writer.js +54 -0
- package/dist/trace-ai/exp/claude-binary.d.ts +5 -0
- package/dist/trace-ai/exp/claude-binary.js +30 -0
- package/dist/trace-ai/exp/coordinator.d.ts +45 -0
- package/dist/trace-ai/exp/coordinator.js +203 -0
- package/dist/trace-ai/exp/eval-runner.d.ts +14 -0
- package/dist/trace-ai/exp/eval-runner.js +47 -0
- package/dist/trace-ai/exp/exp-store/abort-signal.d.ts +3 -0
- package/dist/trace-ai/exp/exp-store/abort-signal.js +27 -0
- package/dist/trace-ai/exp/exp-store/candidate-lineage-yaml.d.ts +4 -0
- package/dist/trace-ai/exp/exp-store/candidate-lineage-yaml.js +37 -0
- package/dist/trace-ai/exp/exp-store/events-jsonl.d.ts +17 -0
- package/dist/trace-ai/exp/exp-store/events-jsonl.js +60 -0
- package/dist/trace-ai/exp/exp-store/exp-registry.d.ts +6 -0
- package/dist/trace-ai/exp/exp-store/exp-registry.js +41 -0
- package/dist/trace-ai/exp/exp-store/index.d.ts +46 -0
- package/dist/trace-ai/exp/exp-store/index.js +59 -0
- package/dist/trace-ai/exp/exp-store/lock.d.ts +3 -0
- package/dist/trace-ai/exp/exp-store/lock.js +73 -0
- package/dist/trace-ai/exp/exp-store/mission-md.d.ts +3 -0
- package/dist/trace-ai/exp/exp-store/mission-md.js +37 -0
- package/dist/trace-ai/exp/exp-store/readme-template.d.ts +5 -0
- package/dist/trace-ai/exp/exp-store/readme-template.js +25 -0
- package/dist/trace-ai/exp/exp-store/round-yaml.d.ts +3 -0
- package/dist/trace-ai/exp/exp-store/round-yaml.js +33 -0
- package/dist/trace-ai/exp/index.d.ts +8 -0
- package/dist/trace-ai/exp/index.js +238 -0
- package/dist/trace-ai/exp/info.d.ts +35 -0
- package/dist/trace-ai/exp/info.js +120 -0
- package/dist/trace-ai/exp/patch/agent-config.d.ts +1 -0
- package/dist/trace-ai/exp/patch/agent-config.js +26 -0
- package/dist/trace-ai/exp/patch/index.d.ts +2 -0
- package/dist/trace-ai/exp/patch/index.js +13 -0
- package/dist/trace-ai/exp/patch/skill.d.ts +1 -0
- package/dist/trace-ai/exp/patch/skill.js +24 -0
- package/dist/trace-ai/exp/providers/synthesizer-client.d.ts +14 -0
- package/dist/trace-ai/exp/providers/synthesizer-client.js +39 -0
- package/dist/trace-ai/exp/providers/triage-client.d.ts +19 -0
- package/dist/trace-ai/exp/providers/triage-client.js +51 -0
- package/dist/trace-ai/exp/schemas.d.ts +147 -0
- package/dist/trace-ai/exp/schemas.js +50 -0
- package/dist/trace-ai/exp/scoring.d.ts +2 -0
- package/dist/trace-ai/exp/scoring.js +46 -0
- package/dist/trace-ai/scan/aggregator.d.ts +20 -0
- package/dist/trace-ai/scan/aggregator.js +26 -0
- package/dist/trace-ai/scan/artifacts/paths.d.ts +12 -0
- package/dist/trace-ai/scan/artifacts/paths.js +18 -0
- package/dist/trace-ai/scan/artifacts/writer.d.ts +67 -0
- package/dist/trace-ai/scan/artifacts/writer.js +96 -0
- package/dist/trace-ai/scan/batched-rubric.d.ts +55 -0
- package/dist/trace-ai/scan/batched-rubric.js +159 -0
- package/dist/trace-ai/scan/cross-trace-synthesizer.d.ts +24 -0
- package/dist/trace-ai/scan/cross-trace-synthesizer.js +93 -0
- package/dist/trace-ai/scan/index.d.ts +31 -0
- package/dist/trace-ai/scan/index.js +390 -0
- package/dist/trace-ai/scan/prompts/builtin/cross-trace-synthesizer-v1.prompt.md +44 -0
- package/dist/trace-ai/scan/prompts/builtin/rubric-judge-batch-v1.prompt.md +44 -0
- package/dist/trace-ai/scan/runner.d.ts +25 -0
- package/dist/trace-ai/scan/runner.js +42 -0
- package/dist/trace-ai/scan/sampler.d.ts +18 -0
- package/dist/trace-ai/scan/sampler.js +81 -0
- package/dist/trace-ai/scan/scan-summary-markdown.d.ts +2 -0
- package/dist/trace-ai/scan/scan-summary-markdown.js +71 -0
- package/dist/trace-ai/scan/scan-summary-schema.d.ts +73 -0
- package/dist/trace-ai/scan/scan-summary-schema.js +61 -0
- package/dist/trace-ai/scan/single-agent-validator.d.ts +23 -0
- package/dist/trace-ai/scan/single-agent-validator.js +42 -0
- package/dist/trace-ai/scan/traces-list-parser.d.ts +15 -0
- package/dist/trace-ai/scan/traces-list-parser.js +46 -0
- package/package.json +14 -4
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M5 eval-set zod schemas (PR-A, MVP-B scope).
|
|
3
|
+
*
|
|
4
|
+
* 4 schemas in this file:
|
|
5
|
+
* - EvalSetIndexSchema: trace-eval-set-index/v1 (eval-set dir's index.yaml)
|
|
6
|
+
* - EvalSetShardSchema: trace-eval-set/v1 (final shard yaml file)
|
|
7
|
+
* - EvalSetInputSchema: trace-eval-set-input/v1 (--queries simplified input)
|
|
8
|
+
* - TestReportSchema: trace-test-report/v1 (test report; PR-A defines schema only;
|
|
9
|
+
* PR-B consumer)
|
|
10
|
+
*
|
|
11
|
+
* EvalSetShardSchema and EvalSetInputSchema share the same refinement:
|
|
12
|
+
* "for each case, at least one of {reference, non-empty assertions} must be present."
|
|
13
|
+
*
|
|
14
|
+
* The D5 builtin rubric `answer-match-reference` output schema is NOT here —
|
|
15
|
+
* it belongs to the rubric template definition (per spec doc §4.1).
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
const InputSchema = z.object({
|
|
19
|
+
user_message: z.string().min(1),
|
|
20
|
+
});
|
|
21
|
+
const ReferenceSchema = z.object({
|
|
22
|
+
answer: z.string().min(1),
|
|
23
|
+
});
|
|
24
|
+
const AssertionSchema = z.object({
|
|
25
|
+
type: z.enum([
|
|
26
|
+
"contains",
|
|
27
|
+
"not_contains",
|
|
28
|
+
"regex",
|
|
29
|
+
"tool_call_count",
|
|
30
|
+
"tool_call_order",
|
|
31
|
+
"semantic_match",
|
|
32
|
+
"latency_ms",
|
|
33
|
+
]),
|
|
34
|
+
}).passthrough(); // allow type-specific fields (value, pattern, tool, op, n, ...)
|
|
35
|
+
// ── trace-eval-set-index/v1 ──────────────────────────────────────────────
|
|
36
|
+
const ShardRefSchema = z.object({
|
|
37
|
+
path: z
|
|
38
|
+
.string()
|
|
39
|
+
.min(1)
|
|
40
|
+
.refine((p) => !p.includes("..") && !p.startsWith("/"), {
|
|
41
|
+
message: "shard path must be a relative path within the eval-set directory (no '..' / '/')",
|
|
42
|
+
}),
|
|
43
|
+
role: z.enum(["seed", "regression", "holdout"]).optional(),
|
|
44
|
+
});
|
|
45
|
+
export const EvalSetIndexSchema = z.object({
|
|
46
|
+
schema_version: z.literal("trace-eval-set-index/v1"),
|
|
47
|
+
eval_set_id: z.string().min(1),
|
|
48
|
+
shards: z.array(ShardRefSchema).min(1),
|
|
49
|
+
});
|
|
50
|
+
// ── trace-eval-set/v1 ────────────────────────────────────────────────────
|
|
51
|
+
const refineCase = (data, ctx) => {
|
|
52
|
+
const hasReference = data.reference !== undefined && data.reference !== null;
|
|
53
|
+
const hasAssertions = Array.isArray(data.assertions) && data.assertions.length > 0;
|
|
54
|
+
if (!hasReference && !hasAssertions) {
|
|
55
|
+
ctx.addIssue({
|
|
56
|
+
code: z.ZodIssueCode.custom,
|
|
57
|
+
message: "each case must have either a 'reference' object or a non-empty 'assertions' array; both empty is not allowed (evaluator has no pass/fail signal)",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const FinalCaseSchema = z
|
|
62
|
+
.object({
|
|
63
|
+
query_id: z.string().min(1),
|
|
64
|
+
input: InputSchema,
|
|
65
|
+
reference: ReferenceSchema.optional(),
|
|
66
|
+
assertions: z.array(AssertionSchema).optional(),
|
|
67
|
+
tags: z.array(z.string()).optional(),
|
|
68
|
+
})
|
|
69
|
+
.superRefine(refineCase);
|
|
70
|
+
export const EvalSetShardSchema = z.object({
|
|
71
|
+
schema_version: z.literal("trace-eval-set/v1"),
|
|
72
|
+
cases: z.array(FinalCaseSchema).min(1),
|
|
73
|
+
});
|
|
74
|
+
// ── trace-eval-set-input/v1 (D1: same refinement as final) ───────────────
|
|
75
|
+
const InputCaseSchema = z
|
|
76
|
+
.object({
|
|
77
|
+
input: InputSchema,
|
|
78
|
+
query_id: z.string().min(1).optional(),
|
|
79
|
+
tags: z.array(z.string()).optional(),
|
|
80
|
+
reference: ReferenceSchema.optional(),
|
|
81
|
+
assertions: z.array(AssertionSchema).optional(),
|
|
82
|
+
})
|
|
83
|
+
.superRefine(refineCase);
|
|
84
|
+
export const EvalSetInputSchema = z.object({
|
|
85
|
+
schema_version: z.literal("trace-eval-set-input/v1"),
|
|
86
|
+
cases: z.array(InputCaseSchema).min(1),
|
|
87
|
+
});
|
|
88
|
+
// ── trace-test-report/v1 (PR-A defines; PR-B writes) ─────────────────────
|
|
89
|
+
const AssertionResultSchema = z.object({
|
|
90
|
+
assertion: AssertionSchema,
|
|
91
|
+
verdict: z.enum(["pass", "fail", "skip"]),
|
|
92
|
+
actual: z.unknown().optional(),
|
|
93
|
+
});
|
|
94
|
+
const CaseResultSchema = z.object({
|
|
95
|
+
query_id: z.string().min(1),
|
|
96
|
+
status: z.enum(["pass", "fail", "error", "skip"]),
|
|
97
|
+
conversation_id: z.string().nullable(),
|
|
98
|
+
trace_id: z.string().nullable().optional(),
|
|
99
|
+
duration_ms: z.number().nonnegative().optional(),
|
|
100
|
+
assertion_results: z.array(AssertionResultSchema),
|
|
101
|
+
failure_reason: z.string().optional(),
|
|
102
|
+
error_code: z.string().optional(),
|
|
103
|
+
error_message: z.string().optional(),
|
|
104
|
+
});
|
|
105
|
+
export const TestReportSchema = z.object({
|
|
106
|
+
schema_version: z.literal("trace-test-report/v1"),
|
|
107
|
+
meta: z.object({
|
|
108
|
+
eval_set_dir: z.string().min(1),
|
|
109
|
+
eval_set_id: z.string().min(1),
|
|
110
|
+
candidate: z.object({
|
|
111
|
+
agent_id: z.string().min(1),
|
|
112
|
+
agent_version: z.string().optional(),
|
|
113
|
+
}),
|
|
114
|
+
cli_version: z.string().min(1),
|
|
115
|
+
ran_at: z.string().min(1),
|
|
116
|
+
duration_ms: z.number().nonnegative(),
|
|
117
|
+
}),
|
|
118
|
+
summary: z.object({
|
|
119
|
+
total: z.number().int().nonnegative(),
|
|
120
|
+
pass: z.number().int().nonnegative(),
|
|
121
|
+
fail: z.number().int().nonnegative(),
|
|
122
|
+
error: z.number().int().nonnegative(),
|
|
123
|
+
skip: z.number().int().nonnegative(),
|
|
124
|
+
by_assertion_type: z.record(z.string(), z.object({
|
|
125
|
+
pass: z.number().int().nonnegative(),
|
|
126
|
+
fail: z.number().int().nonnegative(),
|
|
127
|
+
})),
|
|
128
|
+
}),
|
|
129
|
+
cases: z.array(CaseResultSchema),
|
|
130
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin `semantic_match` judge for the eval-set test runner (M5 D5).
|
|
3
|
+
*
|
|
4
|
+
* Wraps an `AgentProvider` + the `builtin:answer-match-reference` prompt
|
|
5
|
+
* template + a small zod output schema into the `SemanticMatchProvider`
|
|
6
|
+
* surface the assertion-evaluator already speaks. The runner stays
|
|
7
|
+
* provider-agnostic; only this file knows how to render the rubric
|
|
8
|
+
* prompt and validate the LLM's reply.
|
|
9
|
+
*
|
|
10
|
+
* Output schema is intentionally local to this rubric (spec §4.1) —
|
|
11
|
+
* not in `schemas.ts`, which only carries the eval-set / report shapes.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import type { AgentProvider } from "../../agent-providers/types.js";
|
|
15
|
+
import { type PromptTemplateRegistry, type AgentOutputLang } from "../../agent-providers/prompt-template.js";
|
|
16
|
+
import type { SemanticMatchProvider } from "./assertion-evaluator.js";
|
|
17
|
+
export declare const ANSWER_MATCH_REFERENCE_REF = "builtin:answer-match-reference";
|
|
18
|
+
export declare const AnswerMatchOutputSchema: z.ZodObject<{
|
|
19
|
+
verdict: z.ZodEnum<{
|
|
20
|
+
pass: "pass";
|
|
21
|
+
fail: "fail";
|
|
22
|
+
}>;
|
|
23
|
+
reasoning: z.ZodString;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export interface CreateSemanticMatchProviderOpts {
|
|
26
|
+
provider: AgentProvider;
|
|
27
|
+
promptRegistry: PromptTemplateRegistry;
|
|
28
|
+
/** Output locale for the rubric's reasoning text. Default 'en'. */
|
|
29
|
+
lang?: AgentOutputLang;
|
|
30
|
+
/** Per-invoke timeout override. */
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function createBuiltinSemanticMatchProvider(opts: CreateSemanticMatchProviderOpts): SemanticMatchProvider;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin `semantic_match` judge for the eval-set test runner (M5 D5).
|
|
3
|
+
*
|
|
4
|
+
* Wraps an `AgentProvider` + the `builtin:answer-match-reference` prompt
|
|
5
|
+
* template + a small zod output schema into the `SemanticMatchProvider`
|
|
6
|
+
* surface the assertion-evaluator already speaks. The runner stays
|
|
7
|
+
* provider-agnostic; only this file knows how to render the rubric
|
|
8
|
+
* prompt and validate the LLM's reply.
|
|
9
|
+
*
|
|
10
|
+
* Output schema is intentionally local to this rubric (spec §4.1) —
|
|
11
|
+
* not in `schemas.ts`, which only carries the eval-set / report shapes.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { render as renderPrompt, languageInstructionFor, } from "../../agent-providers/prompt-template.js";
|
|
15
|
+
export const ANSWER_MATCH_REFERENCE_REF = "builtin:answer-match-reference";
|
|
16
|
+
export const AnswerMatchOutputSchema = z.object({
|
|
17
|
+
verdict: z.enum(["pass", "fail"]),
|
|
18
|
+
reasoning: z.string(),
|
|
19
|
+
});
|
|
20
|
+
// JSON shape the LLM is told to emit; rendered into the prompt's
|
|
21
|
+
// `{{output_schema}}` placeholder. Kept declarative so we don't try to
|
|
22
|
+
// reflect a Zod schema into JSON at runtime.
|
|
23
|
+
const OUTPUT_SCHEMA_DOC = {
|
|
24
|
+
type: "object",
|
|
25
|
+
required: ["verdict", "reasoning"],
|
|
26
|
+
properties: {
|
|
27
|
+
verdict: { type: "string", enum: ["pass", "fail"] },
|
|
28
|
+
reasoning: { type: "string" },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export function createBuiltinSemanticMatchProvider(opts) {
|
|
32
|
+
const { provider, promptRegistry, lang = "en", timeoutMs } = opts;
|
|
33
|
+
return {
|
|
34
|
+
async judgeSemanticMatch(question, candidateAnswer, referenceAnswer) {
|
|
35
|
+
const tpl = promptRegistry.get(ANSWER_MATCH_REFERENCE_REF);
|
|
36
|
+
const prompt = renderPrompt(tpl, {
|
|
37
|
+
question,
|
|
38
|
+
candidate_answer: candidateAnswer,
|
|
39
|
+
reference_answer: referenceAnswer,
|
|
40
|
+
language_instruction: languageInstructionFor(lang),
|
|
41
|
+
output_schema: OUTPUT_SCHEMA_DOC,
|
|
42
|
+
});
|
|
43
|
+
const resp = await provider.invoke({
|
|
44
|
+
prompt,
|
|
45
|
+
outputSchema: AnswerMatchOutputSchema,
|
|
46
|
+
timeoutMs,
|
|
47
|
+
});
|
|
48
|
+
return { verdict: resp.output.verdict, reasoning: resp.output.reasoning };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TraceSpan } from "../../api/conversations.js";
|
|
2
|
+
import type { SemanticMatchProvider } from "./assertion-evaluator.js";
|
|
3
|
+
export interface RunnerDeps {
|
|
4
|
+
fetchAgent: (agentId: string, version?: string) => Promise<{
|
|
5
|
+
id: string;
|
|
6
|
+
key: string;
|
|
7
|
+
version: string;
|
|
8
|
+
}>;
|
|
9
|
+
sendChat: (opts: {
|
|
10
|
+
agentInfo: {
|
|
11
|
+
id: string;
|
|
12
|
+
key: string;
|
|
13
|
+
version: string;
|
|
14
|
+
};
|
|
15
|
+
query: string;
|
|
16
|
+
conversationId?: string;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
text: string;
|
|
19
|
+
conversationId?: string;
|
|
20
|
+
}>;
|
|
21
|
+
fetchTrace: (conversationId: string) => Promise<{
|
|
22
|
+
spans: TraceSpan[];
|
|
23
|
+
}>;
|
|
24
|
+
semanticMatchProvider?: SemanticMatchProvider;
|
|
25
|
+
}
|
|
26
|
+
export interface RunOpts {
|
|
27
|
+
evalSetDir: string;
|
|
28
|
+
candidateAgentId: string;
|
|
29
|
+
candidateAgentVersion?: string;
|
|
30
|
+
outDir: string;
|
|
31
|
+
maxParallel?: number;
|
|
32
|
+
deps: RunnerDeps;
|
|
33
|
+
}
|
|
34
|
+
export declare function run(opts: RunOpts): Promise<void>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { evaluateAssertion } from "./assertion-evaluator.js";
|
|
5
|
+
import { EvalSetIndexSchema, EvalSetShardSchema, TestReportSchema } from "./schemas.js";
|
|
6
|
+
// ── eval-set loader ───────────────────────────────────────────────────────────
|
|
7
|
+
async function loadEvalCases(evalSetDir) {
|
|
8
|
+
const indexRaw = await fs.readFile(path.join(evalSetDir, "index.yaml"), "utf8");
|
|
9
|
+
const index = EvalSetIndexSchema.parse(yaml.load(indexRaw));
|
|
10
|
+
const cases = [];
|
|
11
|
+
for (const shard of index.shards) {
|
|
12
|
+
const shardRaw = await fs.readFile(path.join(evalSetDir, shard.path), "utf8");
|
|
13
|
+
const parsed = EvalSetShardSchema.parse(yaml.load(shardRaw));
|
|
14
|
+
cases.push(...parsed.cases);
|
|
15
|
+
}
|
|
16
|
+
return cases;
|
|
17
|
+
}
|
|
18
|
+
// ── case runner ───────────────────────────────────────────────────────────────
|
|
19
|
+
async function runCase(evalCase, agentInfo, deps) {
|
|
20
|
+
const startMs = Date.now();
|
|
21
|
+
let conversationId = null;
|
|
22
|
+
let traceId = null;
|
|
23
|
+
let spans = [];
|
|
24
|
+
let answerText = "";
|
|
25
|
+
let stage = "chat";
|
|
26
|
+
try {
|
|
27
|
+
const chatResult = await deps.sendChat({
|
|
28
|
+
agentInfo,
|
|
29
|
+
query: evalCase.input.user_message,
|
|
30
|
+
});
|
|
31
|
+
answerText = chatResult.text;
|
|
32
|
+
conversationId = chatResult.conversationId ?? null;
|
|
33
|
+
if (conversationId) {
|
|
34
|
+
stage = "trace";
|
|
35
|
+
const traceResult = await deps.fetchTrace(conversationId);
|
|
36
|
+
spans = traceResult.spans;
|
|
37
|
+
traceId = spans[0]?.traceId ?? null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
const durationMs = Date.now() - startMs;
|
|
42
|
+
return {
|
|
43
|
+
query_id: evalCase.query_id,
|
|
44
|
+
status: "error",
|
|
45
|
+
conversation_id: conversationId,
|
|
46
|
+
trace_id: traceId,
|
|
47
|
+
duration_ms: durationMs,
|
|
48
|
+
assertion_results: [],
|
|
49
|
+
error_message: e instanceof Error ? e.message : String(e),
|
|
50
|
+
error_code: stage === "trace" ? "trace-fetch-failed" : "chat-failed",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const durationMs = Date.now() - startMs;
|
|
54
|
+
const assertionResults = [];
|
|
55
|
+
for (const assertion of evalCase.assertions ?? []) {
|
|
56
|
+
try {
|
|
57
|
+
const result = await evaluateAssertion(assertion, {
|
|
58
|
+
answer: answerText,
|
|
59
|
+
spans,
|
|
60
|
+
reference: evalCase.reference,
|
|
61
|
+
durationMs,
|
|
62
|
+
question: evalCase.input.user_message,
|
|
63
|
+
semanticMatchProvider: deps.semanticMatchProvider,
|
|
64
|
+
});
|
|
65
|
+
assertionResults.push({ assertion, verdict: result.verdict, actual: result.actual });
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
assertionResults.push({ assertion, verdict: "skip", actual: `assertion-eval-error: ${e.message}` });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// A case may pass schema with reference-only (no assertions), but without
|
|
72
|
+
// assertions there is no pass/fail signal — mark skip so it does not
|
|
73
|
+
// silently inflate the pass count.
|
|
74
|
+
if (assertionResults.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
query_id: evalCase.query_id,
|
|
77
|
+
status: "skip",
|
|
78
|
+
conversation_id: conversationId,
|
|
79
|
+
trace_id: traceId,
|
|
80
|
+
duration_ms: durationMs,
|
|
81
|
+
assertion_results: assertionResults,
|
|
82
|
+
failure_reason: "no assertions configured; case has reference but no judge (e.g. semantic_match) wired",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const hasFail = assertionResults.some((r) => r.verdict === "fail");
|
|
86
|
+
const allSkip = assertionResults.every((r) => r.verdict === "skip");
|
|
87
|
+
const status = hasFail ? "fail" : allSkip ? "skip" : "pass";
|
|
88
|
+
return {
|
|
89
|
+
query_id: evalCase.query_id,
|
|
90
|
+
status: status,
|
|
91
|
+
conversation_id: conversationId,
|
|
92
|
+
trace_id: traceId,
|
|
93
|
+
duration_ms: durationMs,
|
|
94
|
+
assertion_results: assertionResults,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// ── main runner ───────────────────────────────────────────────────────────────
|
|
98
|
+
export async function run(opts) {
|
|
99
|
+
const { evalSetDir, candidateAgentId, candidateAgentVersion, outDir, deps } = opts;
|
|
100
|
+
const maxParallel = opts.maxParallel ?? 4;
|
|
101
|
+
const [cases, agentInfo] = await Promise.all([
|
|
102
|
+
loadEvalCases(evalSetDir),
|
|
103
|
+
deps.fetchAgent(candidateAgentId, candidateAgentVersion),
|
|
104
|
+
]);
|
|
105
|
+
// Fetch eval_set_id from index
|
|
106
|
+
const indexRaw = await fs.readFile(path.join(evalSetDir, "index.yaml"), "utf8");
|
|
107
|
+
const index = EvalSetIndexSchema.parse(yaml.load(indexRaw));
|
|
108
|
+
const ranAt = new Date().toISOString();
|
|
109
|
+
const overallStart = Date.now();
|
|
110
|
+
const caseResults = new Array(cases.length);
|
|
111
|
+
let nextIdx = 0;
|
|
112
|
+
const workerCount = Math.max(1, Math.min(maxParallel, cases.length));
|
|
113
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
114
|
+
while (true) {
|
|
115
|
+
const idx = nextIdx++;
|
|
116
|
+
if (idx >= cases.length)
|
|
117
|
+
return;
|
|
118
|
+
caseResults[idx] = await runCase(cases[idx], agentInfo, deps);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
await Promise.all(workers);
|
|
122
|
+
const overallDurationMs = Date.now() - overallStart;
|
|
123
|
+
// Build summary
|
|
124
|
+
const counts = { total: caseResults.length, pass: 0, fail: 0, error: 0, skip: 0 };
|
|
125
|
+
const byType = {};
|
|
126
|
+
for (const cr of caseResults) {
|
|
127
|
+
counts[cr.status]++;
|
|
128
|
+
for (const ar of cr.assertion_results) {
|
|
129
|
+
const t = ar.assertion["type"];
|
|
130
|
+
if (!byType[t])
|
|
131
|
+
byType[t] = { pass: 0, fail: 0 };
|
|
132
|
+
if (ar.verdict === "pass")
|
|
133
|
+
byType[t].pass++;
|
|
134
|
+
else if (ar.verdict === "fail")
|
|
135
|
+
byType[t].fail++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const report = TestReportSchema.parse({
|
|
139
|
+
schema_version: "trace-test-report/v1",
|
|
140
|
+
meta: {
|
|
141
|
+
eval_set_dir: evalSetDir,
|
|
142
|
+
eval_set_id: index.eval_set_id,
|
|
143
|
+
candidate: { agent_id: agentInfo.id, agent_version: agentInfo.version },
|
|
144
|
+
cli_version: "0.0.0",
|
|
145
|
+
ran_at: ranAt,
|
|
146
|
+
duration_ms: overallDurationMs,
|
|
147
|
+
},
|
|
148
|
+
summary: { ...counts, by_assertion_type: byType },
|
|
149
|
+
cases: caseResults,
|
|
150
|
+
});
|
|
151
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
152
|
+
await fs.writeFile(path.join(outDir, "report.yaml"), yaml.dump(report, { lineWidth: 120 }), "utf8");
|
|
153
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal types for the M5 eval-set module (PR-A).
|
|
3
|
+
*
|
|
4
|
+
* These mirror the zod schemas in `./schemas.ts` but are kept independent so
|
|
5
|
+
* non-validating code paths (builder / picker / redactor / output-writer) can
|
|
6
|
+
* import the types without paying the zod parse overhead at module load.
|
|
7
|
+
*/
|
|
8
|
+
export interface EvalCaseInput {
|
|
9
|
+
user_message: string;
|
|
10
|
+
}
|
|
11
|
+
export interface EvalReference {
|
|
12
|
+
answer: string;
|
|
13
|
+
}
|
|
14
|
+
export type AssertionType = "contains" | "not_contains" | "regex" | "tool_call_count" | "tool_call_order" | "semantic_match" | "latency_ms";
|
|
15
|
+
export interface EvalAssertion {
|
|
16
|
+
type: AssertionType;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface EvalCase {
|
|
20
|
+
query_id: string;
|
|
21
|
+
input: EvalCaseInput;
|
|
22
|
+
reference?: EvalReference;
|
|
23
|
+
assertions?: EvalAssertion[];
|
|
24
|
+
tags?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface EvalSetIndexShard {
|
|
27
|
+
path: string;
|
|
28
|
+
role?: "seed" | "regression" | "holdout";
|
|
29
|
+
}
|
|
30
|
+
export interface EvalSetIndex {
|
|
31
|
+
schema_version: "trace-eval-set-index/v1";
|
|
32
|
+
eval_set_id: string;
|
|
33
|
+
shards: EvalSetIndexShard[];
|
|
34
|
+
}
|
|
35
|
+
export interface BuildResult {
|
|
36
|
+
cases_written: number;
|
|
37
|
+
cases_skipped: number;
|
|
38
|
+
conflicts: string[];
|
|
39
|
+
shard_paths: string[];
|
|
40
|
+
redaction_rules_source: "cli-flag" | "repo" | "builtin";
|
|
41
|
+
}
|
|
42
|
+
export interface RedactionRule {
|
|
43
|
+
name: string;
|
|
44
|
+
pattern: RegExp;
|
|
45
|
+
replace: string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal types for the M5 eval-set module (PR-A).
|
|
3
|
+
*
|
|
4
|
+
* These mirror the zod schemas in `./schemas.ts` but are kept independent so
|
|
5
|
+
* non-validating code paths (builder / picker / redactor / output-writer) can
|
|
6
|
+
* import the types without paying the zod parse overhead at module load.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LineageEntry, RoundData } from "./schemas.js";
|
|
2
|
+
interface WriteBundlesOpts {
|
|
3
|
+
expDir: string;
|
|
4
|
+
experimentId: string;
|
|
5
|
+
lineage: LineageEntry[];
|
|
6
|
+
rounds: RoundData[];
|
|
7
|
+
createdBy: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function writeBundles(opts: WriteBundlesOpts): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/trace-ai/exp/bundle-writer.ts
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
export async function writeBundles(opts) {
|
|
7
|
+
const { expDir, experimentId, lineage, rounds, createdBy } = opts;
|
|
8
|
+
const bestEntry = lineage.filter(e => e.status === "scored").at(-1) ?? lineage.at(-1);
|
|
9
|
+
const bestVersion = bestEntry?.version ?? 0;
|
|
10
|
+
const bundleId = `bundle_${crypto.randomBytes(4).toString("hex")}`;
|
|
11
|
+
const now = new Date().toISOString();
|
|
12
|
+
const bundle = {
|
|
13
|
+
schema_version: "trace-bundle/v1",
|
|
14
|
+
experiment_id: experimentId,
|
|
15
|
+
bundle_id: bundleId,
|
|
16
|
+
best_trial_version: bestVersion,
|
|
17
|
+
resources: {
|
|
18
|
+
agent_config: bestEntry?.next_change ?? {},
|
|
19
|
+
skills: [],
|
|
20
|
+
},
|
|
21
|
+
provenance: {
|
|
22
|
+
created_by: createdBy,
|
|
23
|
+
created_at: now,
|
|
24
|
+
evidence_traces: rounds.flatMap(r => (r.per_query_results ?? []).map(q => q.raw_trace_id ?? "").filter(Boolean)),
|
|
25
|
+
round_refs: rounds.map(r => `.trace-state/rounds/round-${r.round}.yaml`),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const lastRound = rounds.at(-1);
|
|
29
|
+
const manifest = {
|
|
30
|
+
schema_version: "trace-manifest/v1",
|
|
31
|
+
experiment_id: experimentId,
|
|
32
|
+
trial_version: bestVersion,
|
|
33
|
+
predictions: {
|
|
34
|
+
fixes: (lastRound?.per_query_results ?? [])
|
|
35
|
+
.filter(q => q.assertion_results.every(a => a.verdict === "pass"))
|
|
36
|
+
.map(q => ({ query_id: q.query_id, reason: "all assertions passed" })),
|
|
37
|
+
risks: (lastRound?.per_query_results ?? [])
|
|
38
|
+
.filter(q => q.assertion_results.some(a => a.verdict === "fail"))
|
|
39
|
+
.map(q => ({ query_id: q.query_id, reason: "assertions failed" })),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const provenance = {
|
|
43
|
+
experiment_id: experimentId,
|
|
44
|
+
generated_at: now,
|
|
45
|
+
rounds_count: rounds.length,
|
|
46
|
+
lineage_count: lineage.length,
|
|
47
|
+
round_verdicts: rounds.map(r => ({ round: r.round, verdict: r.triage_conclusion?.verdict ?? "pending" })),
|
|
48
|
+
};
|
|
49
|
+
const outDir = path.join(expDir, "outputs");
|
|
50
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
51
|
+
await fs.writeFile(path.join(outDir, "bundle.yaml"), yaml.dump(bundle, { lineWidth: -1 }));
|
|
52
|
+
await fs.writeFile(path.join(outDir, "manifest.yaml"), yaml.dump(manifest, { lineWidth: -1 }));
|
|
53
|
+
await fs.writeFile(path.join(outDir, "provenance.yaml"), yaml.dump(provenance, { lineWidth: -1 }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the path to the claude CLI binary.
|
|
5
|
+
* Priority: CLAUDE_BIN env → `which claude` → known install locations → bare "claude".
|
|
6
|
+
*/
|
|
7
|
+
export function resolveClaudeBinary() {
|
|
8
|
+
if (process.env["CLAUDE_BIN"])
|
|
9
|
+
return process.env["CLAUDE_BIN"];
|
|
10
|
+
try {
|
|
11
|
+
const resolved = execSync("which claude", { encoding: "utf8", timeout: 3000 }).trim();
|
|
12
|
+
// Reject shell alias expansions like "claude: aliased to ..."
|
|
13
|
+
if (resolved && !resolved.includes(" "))
|
|
14
|
+
return resolved;
|
|
15
|
+
}
|
|
16
|
+
catch { /* fall through */ }
|
|
17
|
+
const home = os.homedir();
|
|
18
|
+
for (const p of [
|
|
19
|
+
`${home}/.local/bin/claude`,
|
|
20
|
+
"/opt/homebrew/bin/claude",
|
|
21
|
+
"/usr/local/bin/claude",
|
|
22
|
+
]) {
|
|
23
|
+
try {
|
|
24
|
+
execSync(`test -x "${p}"`, { timeout: 1000 });
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
catch { /* try next */ }
|
|
28
|
+
}
|
|
29
|
+
return "claude";
|
|
30
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Mission, NextChange, QueryResult, RoundData } from "./schemas.js";
|
|
2
|
+
export interface SynthesizerClient {
|
|
3
|
+
generate(input: {
|
|
4
|
+
mission: Mission;
|
|
5
|
+
candidateConfig: Record<string, unknown>;
|
|
6
|
+
prevRound?: RoundData;
|
|
7
|
+
prevRounds: RoundData[];
|
|
8
|
+
crossRoundMemoryRef?: string;
|
|
9
|
+
}): Promise<NextChange>;
|
|
10
|
+
}
|
|
11
|
+
export interface TriageClient {
|
|
12
|
+
triage(input: {
|
|
13
|
+
currentRound: RoundData;
|
|
14
|
+
prevRounds: RoundData[];
|
|
15
|
+
candidateConfig: Record<string, unknown>;
|
|
16
|
+
crossRoundMemoryRef?: string;
|
|
17
|
+
}): Promise<RoundData["triage_conclusion"] & {
|
|
18
|
+
new_memory_token: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export interface CoordinatorOpts {
|
|
22
|
+
expDir: string;
|
|
23
|
+
synthesizer: SynthesizerClient;
|
|
24
|
+
triage: TriageClient;
|
|
25
|
+
runEval: (opts: {
|
|
26
|
+
evalSetPaths: string[];
|
|
27
|
+
candidatePath: string;
|
|
28
|
+
expDir: string;
|
|
29
|
+
round: number;
|
|
30
|
+
}) => Promise<{
|
|
31
|
+
queryResults: QueryResult[];
|
|
32
|
+
}>;
|
|
33
|
+
experimentId?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare class ExperimentCoordinator {
|
|
36
|
+
private opts;
|
|
37
|
+
private store;
|
|
38
|
+
private heartbeatTimer?;
|
|
39
|
+
constructor(opts: CoordinatorOpts);
|
|
40
|
+
run(): Promise<void>;
|
|
41
|
+
resume(): Promise<void>;
|
|
42
|
+
private runLoop;
|
|
43
|
+
private checkAbort;
|
|
44
|
+
private withRetry;
|
|
45
|
+
}
|