Merge pull request #1916 from pikasTech/fix/artificer-compact-output
fix: 收敛 Artificer Target 派单输出
This commit is contained in:
@@ -45,6 +45,12 @@ bun scripts/cli.ts agentrun create task \
|
||||
- 未显式传 `--idempotency-key` 时,由 Target/repo/ref/MDTODO/verified commit 派生稳定键;
|
||||
- 幂等重试遇到已有 running 或 terminal task 时,返回既有 attempt/run/session,不重复 dispatch;
|
||||
- 最终输出必须包含 task、attempt、run、command、runner 和 session identity。
|
||||
- Target live 的默认 human、`-o json` 和 `-o yaml` 使用同一份有界投影:
|
||||
- 保留 task、attempt、run、command、runner、session 的 identity 与 state;
|
||||
- 保留自动派单决策、Target/MDTODO 摘要、Aipod 与 SecretRef presence 摘要;
|
||||
- `Next` 使用本次派单的具体 identity,并继承 Target 与 lane;
|
||||
- 不展开完整 task、prompt、payload 或 Aipod 对象;
|
||||
- 需要完整服务端对象时显式使用 `--raw`。
|
||||
|
||||
Artificer 内部执行边界:
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { readFileSync, rmSync } from "node:fs";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { parseAgentRunClientConfigYaml, resolveAgentRunAuth, runAgentRunCommand, stripAgentRunResourceWrapperArgs } from "./agentrun";
|
||||
import {
|
||||
parseAgentRunClientConfigYaml,
|
||||
parseResourceOptions,
|
||||
renderMutationSummary,
|
||||
resolveAgentRunAuth,
|
||||
runAgentRunCommand,
|
||||
stripAgentRunResourceWrapperArgs,
|
||||
} from "./agentrun";
|
||||
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
|
||||
import { agentRunToolCredentialRefs } from "./agentrun/config";
|
||||
import { autoDispatchTargetTask, targetTaskDispatchDryRunPlan } from "./agentrun/rest-bridge";
|
||||
@@ -318,6 +325,200 @@ describe("Artificer Target one-command dispatch", () => {
|
||||
rmSync(tempConfigPath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps Target live human, JSON, and YAML output bounded while retaining dispatch identities", () => {
|
||||
const outputConfig = Bun.YAML.parse(
|
||||
readFileSync(new URL("../../config/unidesk-cli.yaml", import.meta.url), "utf8"),
|
||||
) as { output?: { maxStdoutBytes?: number } };
|
||||
const maxStdoutBytes = outputConfig.output?.maxStdoutBytes;
|
||||
if (typeof maxStdoutBytes !== "number") throw new Error("expected YAML-first output.maxStdoutBytes");
|
||||
const hugePrompt = "PROMPT_SHOULD_NOT_BE_RENDERED_".repeat(500);
|
||||
expect(Buffer.byteLength(hugePrompt, "utf8")).toBeGreaterThan(maxStdoutBytes);
|
||||
|
||||
const identity = {
|
||||
taskId: "qt_compact_target",
|
||||
taskState: "running",
|
||||
attemptId: "qat_compact_target",
|
||||
runId: "run_compact_target",
|
||||
commandId: "cmd_compact_target",
|
||||
runnerJobId: "rjob_compact_target",
|
||||
sessionId: "sess_compact_target",
|
||||
valuesPrinted: false,
|
||||
};
|
||||
const autoDispatch = {
|
||||
enabled: true,
|
||||
decision: "dispatched",
|
||||
mutation: true,
|
||||
duplicateDispatchPrevented: false,
|
||||
identity,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
const raw = {
|
||||
ok: true,
|
||||
action: "queue-submit-auto-dispatch",
|
||||
data: {
|
||||
...identity,
|
||||
attemptState: "running",
|
||||
runState: "running",
|
||||
commandState: "running",
|
||||
runnerState: "claimed",
|
||||
sessionState: "active",
|
||||
mutation: true,
|
||||
autoDispatch,
|
||||
task: {
|
||||
id: identity.taskId,
|
||||
state: identity.taskState,
|
||||
prompt: hugePrompt,
|
||||
payload: { prompt: hugePrompt },
|
||||
latestAttempt: {
|
||||
attemptId: identity.attemptId,
|
||||
state: "running",
|
||||
runnerJobId: identity.runnerJobId,
|
||||
runnerState: "claimed",
|
||||
sessionId: identity.sessionId,
|
||||
},
|
||||
},
|
||||
run: {
|
||||
id: identity.runId,
|
||||
state: "running",
|
||||
sessionRef: { sessionId: identity.sessionId, state: "active" },
|
||||
},
|
||||
command: { id: identity.commandId, state: "running", payload: { prompt: hugePrompt } },
|
||||
latestAttempt: {
|
||||
attemptId: identity.attemptId,
|
||||
state: "running",
|
||||
runId: identity.runId,
|
||||
commandId: identity.commandId,
|
||||
runnerJobId: identity.runnerJobId,
|
||||
runnerState: "claimed",
|
||||
sessionId: identity.sessionId,
|
||||
},
|
||||
session: { id: identity.sessionId, state: "active", prompt: hugePrompt },
|
||||
valuesPrinted: false,
|
||||
},
|
||||
targetTask: {
|
||||
mdtodoId: "R6.4",
|
||||
target: "NC01",
|
||||
targetWorkspace: "/root/.worktrees/unidesk/artificer-compact-output",
|
||||
targetRoute: "NC01:/root/.worktrees/unidesk/artificer-compact-output",
|
||||
projectId: "pikasTech/unidesk",
|
||||
repository: "pikasTech/unidesk",
|
||||
ref: "fix/artificer-compact-output",
|
||||
verifiedCommit: "a".repeat(40),
|
||||
refRelation: "branch-and-head-match-remote-ref",
|
||||
workspace: {
|
||||
branch: "fix/artificer-compact-output",
|
||||
headCommit: "a".repeat(40),
|
||||
clean: true,
|
||||
},
|
||||
prompt: hugePrompt,
|
||||
},
|
||||
aipodBinding: {
|
||||
aipod: "Artificer",
|
||||
node: "NC01",
|
||||
lane: "nc01-v02",
|
||||
namespace: "agentrun-v01",
|
||||
projectId: "pikasTech/unidesk",
|
||||
providerId: "NC01",
|
||||
backendProfile: "sub2api",
|
||||
workspaceRef: { kind: "opaque", path: ".", valid: true },
|
||||
resourceBundleRef: {
|
||||
kind: "gitbundle",
|
||||
repoUrl: "https://credential-must-be-redacted@github.com/pikasTech/unidesk.git",
|
||||
ref: "fix/artificer-compact-output",
|
||||
inheritedFromAipod: true,
|
||||
credentialRefPresent: true,
|
||||
},
|
||||
session: { sessionId: identity.sessionId, identitySource: "task" },
|
||||
providerCredentials: [{
|
||||
profile: "sub2api",
|
||||
secretRef: { namespace: "agentrun-v01", name: "agentrun-provider", keys: ["auth.json"] },
|
||||
bindingPresent: true,
|
||||
value: hugePrompt,
|
||||
}],
|
||||
toolCredentials: [{
|
||||
tool: "github",
|
||||
purpose: "pull-request",
|
||||
secretRef: { namespace: "agentrun-v01", name: "agentrun-github", keys: ["GH_TOKEN"] },
|
||||
bindingPresent: true,
|
||||
value: hugePrompt,
|
||||
}],
|
||||
prompt: hugePrompt,
|
||||
},
|
||||
autoDispatch,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
expect(Buffer.byteLength(JSON.stringify(raw), "utf8")).toBeGreaterThan(maxStdoutBytes);
|
||||
|
||||
const variants = [
|
||||
{ name: "human", args: [] },
|
||||
{ name: "json", args: ["-o", "json"] },
|
||||
{ name: "yaml", args: ["-o", "yaml"] },
|
||||
];
|
||||
for (const variant of variants) {
|
||||
const rendered = renderMutationSummary(
|
||||
"agentrun queue submit",
|
||||
raw,
|
||||
parseResourceOptions(variant.args),
|
||||
"Queue task submitted",
|
||||
);
|
||||
const text = renderedTextOf(rendered);
|
||||
expect(Buffer.byteLength(text, "utf8"), variant.name).toBeLessThan(maxStdoutBytes);
|
||||
expect(text).not.toContain("PROMPT_SHOULD_NOT_BE_RENDERED_");
|
||||
expect(text).not.toContain("credential-must-be-redacted");
|
||||
for (const value of Object.values(identity).filter((item): item is string => typeof item === "string")) {
|
||||
expect(text, `${variant.name}:${value}`).toContain(value);
|
||||
}
|
||||
expect(text).toContain("R6.4");
|
||||
expect(text).toContain("dispatched");
|
||||
}
|
||||
|
||||
const jsonText = renderedTextOf(renderMutationSummary(
|
||||
"agentrun queue submit",
|
||||
raw,
|
||||
parseResourceOptions(["-o", "json"]),
|
||||
"Queue task submitted",
|
||||
));
|
||||
const json = JSON.parse(jsonText) as Record<string, unknown>;
|
||||
const jsonData = json.data as Record<string, unknown>;
|
||||
const jsonNext = json.next as Record<string, unknown>;
|
||||
const jsonCredentials = (json.aipodBinding as Record<string, unknown>).credentialRefs as Record<string, unknown>;
|
||||
expect(jsonData).toMatchObject({
|
||||
taskId: identity.taskId,
|
||||
taskState: "running",
|
||||
attemptId: identity.attemptId,
|
||||
attemptState: "running",
|
||||
runId: identity.runId,
|
||||
runState: "running",
|
||||
commandId: identity.commandId,
|
||||
commandState: "running",
|
||||
runnerJobId: identity.runnerJobId,
|
||||
runnerState: "claimed",
|
||||
sessionId: identity.sessionId,
|
||||
sessionState: "active",
|
||||
});
|
||||
expect((json.autoDispatch as Record<string, unknown>).decision).toBe("dispatched");
|
||||
expect((json.targetTask as Record<string, unknown>).mdtodoId).toBe("R6.4");
|
||||
expect(jsonCredentials).toMatchObject({ totalCount: 2, allPresent: true });
|
||||
expect((jsonCredentials.provider as Record<string, unknown>).presentCount).toBe(1);
|
||||
expect((jsonCredentials.tool as Record<string, unknown>).presentCount).toBe(1);
|
||||
expect(jsonNext.result).toBe(
|
||||
`bun scripts/cli.ts agentrun result run/${identity.runId} --command ${identity.commandId} --target NC01 --lane nc01-v02`,
|
||||
);
|
||||
expect(jsonNext.logs).toBe(
|
||||
`bun scripts/cli.ts agentrun logs session/${identity.sessionId} --tail 120 --target NC01 --lane nc01-v02`,
|
||||
);
|
||||
|
||||
const yamlText = renderedTextOf(renderMutationSummary(
|
||||
"agentrun queue submit",
|
||||
raw,
|
||||
parseResourceOptions(["-o", "yaml"]),
|
||||
"Queue task submitted",
|
||||
));
|
||||
const yaml = Bun.YAML.parse(yamlText) as Record<string, unknown>;
|
||||
expect(yaml.data).toMatchObject(json.data as object);
|
||||
expect(yaml.next).toMatchObject(json.next as object);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentRun default transport contract", () => {
|
||||
|
||||
@@ -39,6 +39,7 @@ import { status } from "./control-plane";
|
||||
import { arrayRecords, displayValue, isRecord, nextPagedResourceCommand, parseTaskManifest, pickCompact, relativeAge, renderFailureLines, renderResourceNextLines, renderTable, resourceName, shortId, stringOrDash, truncateMultiline, truncateOneLine } from "./options";
|
||||
import { activeAgentRunRestTarget, agentRunRestRequest, runAgentRunRestCommand } from "./rest-bridge";
|
||||
import { renderSessionSendResult } from "./session-send-render";
|
||||
import { compactTargetTaskMutationPayload } from "./target-task-output";
|
||||
import { record, stringOrNull } from "./utils";
|
||||
|
||||
export function resolveAgentRunCancelPolicyTarget(config: UniDeskConfig | null, options: AgentRunResourceOptions): { configPath: string; spec: AgentRunLaneSpec; source: "selected-lane" | "default-lane" } | null {
|
||||
@@ -479,7 +480,10 @@ export function renderResultSummary(command: string, raw: Record<string, unknown
|
||||
|
||||
export function renderMutationSummary(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, headline: string, overrideNextLines?: string[]): RenderedCliResult {
|
||||
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
||||
if (options.output === "json" || options.output === "yaml") return renderMachine(command, raw, options.output, raw.ok !== false);
|
||||
const targetTaskPayload = compactTargetTaskMutationPayload(raw);
|
||||
if (options.output === "json" || options.output === "yaml") {
|
||||
return renderMachine(command, targetTaskPayload ?? raw, options.output, raw.ok !== false);
|
||||
}
|
||||
const data = record(innerData(raw));
|
||||
const id = stringOrNull(data.id) ?? stringOrNull(data.taskId) ?? stringOrNull(data.sessionId);
|
||||
const lines = [
|
||||
@@ -496,12 +500,13 @@ export function renderMutationSummary(command: string, raw: Record<string, unkno
|
||||
if (decision !== null) lines.push(`Decision: ${decision}`);
|
||||
if (internalCommandType !== null) lines.push(`InternalCommandType: ${internalCommandType}`);
|
||||
lines.push(...renderCancelLifecycleMutationLines(record(data.cancelLifecycle ?? raw.cancelLifecycle)));
|
||||
lines.push(...renderTargetTaskMutationLines(record(data.targetTask ?? raw.targetTask)));
|
||||
lines.push(...renderAipodBindingMutationLines(record(data.aipodBinding ?? raw.aipodBinding)));
|
||||
lines.push(...renderAutoDispatchMutationLines(record(data.autoDispatch ?? raw.autoDispatch)));
|
||||
lines.push(...renderTargetTaskMutationLines(record(targetTaskPayload?.targetTask ?? data.targetTask ?? raw.targetTask)));
|
||||
lines.push(...renderAipodBindingMutationLines(record(targetTaskPayload?.aipodBinding ?? data.aipodBinding ?? raw.aipodBinding)));
|
||||
lines.push(...renderAutoDispatchMutationLines(record(targetTaskPayload?.autoDispatch ?? data.autoDispatch ?? raw.autoDispatch)));
|
||||
lines.push(...renderDispatchPlanMutationLines(record(data.dispatchPlan ?? raw.dispatchPlan)));
|
||||
const next = record(raw.next ?? data.next);
|
||||
const nextLines = (overrideNextLines ?? Object.values(next).map(String)).filter((line) => line.length > 0).slice(0, 5);
|
||||
const next = record(targetTaskPayload?.next ?? raw.next ?? data.next);
|
||||
const nextLimit = targetTaskPayload === null ? 5 : 8;
|
||||
const nextLines = (overrideNextLines ?? Object.values(next).map(String)).filter((line) => line.length > 0).slice(0, nextLimit);
|
||||
if (nextLines.length > 0) lines.push("", "Next:", ...nextLines.map((line) => ` ${line}`));
|
||||
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
|
||||
}
|
||||
@@ -525,8 +530,11 @@ export function renderAipodBindingMutationLines(binding: Record<string, unknown>
|
||||
const workspaceRef = record(binding.workspaceRef);
|
||||
const resourceBundleRef = record(binding.resourceBundleRef);
|
||||
const session = record(binding.session);
|
||||
const providerCredentials = arrayRecords(binding.providerCredentials);
|
||||
const toolCredentials = arrayRecords(binding.toolCredentials);
|
||||
const credentialRefs = record(binding.credentialRefs);
|
||||
const providerSummary = record(credentialRefs.provider);
|
||||
const toolSummary = record(credentialRefs.tool);
|
||||
const providerCredentials = arrayRecords(binding.providerCredentials ?? providerSummary.items);
|
||||
const toolCredentials = arrayRecords(binding.toolCredentials ?? toolSummary.items);
|
||||
const lines = [
|
||||
"",
|
||||
"AipodBinding:",
|
||||
@@ -535,6 +543,16 @@ export function renderAipodBindingMutationLines(binding: Record<string, unknown>
|
||||
` ResourceBundle: ${displayValue(resourceBundleRef.repoUrl)} ref=${displayValue(resourceBundleRef.ref)} inherited=${displayValue(resourceBundleRef.inheritedFromAipod)}`,
|
||||
` Session: ${displayValue(session.sessionId)} source=${displayValue(session.identitySource)}`,
|
||||
];
|
||||
if (Object.keys(credentialRefs).length > 0) {
|
||||
const providerPresence = `${displayValue(providerSummary.presentCount)}/${displayValue(providerSummary.count)}`;
|
||||
const toolPresence = `${displayValue(toolSummary.presentCount)}/${displayValue(toolSummary.count)}`;
|
||||
lines.push([
|
||||
` SecretRefs: total=${displayValue(credentialRefs.totalCount)}`,
|
||||
`allPresent=${displayValue(credentialRefs.allPresent)}`,
|
||||
`provider=${providerPresence}`,
|
||||
`tool=${toolPresence}`,
|
||||
].join(" "));
|
||||
}
|
||||
for (const credential of providerCredentials.slice(0, 4)) {
|
||||
const secretRef = record(credential.secretRef);
|
||||
lines.push(` ProviderSecretRef: ${displayValue(secretRef.namespace)}/${displayValue(secretRef.name)} keys=${displayValue(secretRef.keys)} present=${displayValue(credential.bindingPresent)}`);
|
||||
@@ -553,9 +571,12 @@ export function renderAutoDispatchMutationLines(autoDispatch: Record<string, unk
|
||||
"",
|
||||
"AutoDispatch:",
|
||||
` Decision: ${displayValue(autoDispatch.decision)} mutation=${displayValue(autoDispatch.mutation)} duplicatePrevented=${displayValue(autoDispatch.duplicateDispatchPrevented)}`,
|
||||
` Task: ${displayValue(identity.taskId)} state=${displayValue(identity.taskState)} attempt=${displayValue(identity.attemptId)}`,
|
||||
` Run: ${displayValue(identity.runId)} command=${displayValue(identity.commandId)} runner=${displayValue(identity.runnerJobId)}`,
|
||||
` Session: ${displayValue(identity.sessionId)}`,
|
||||
` Task: ${displayValue(identity.taskId)} state=${displayValue(identity.taskState)}`,
|
||||
` Attempt: ${displayValue(identity.attemptId)} state=${displayValue(identity.attemptState)}`,
|
||||
` Run: ${displayValue(identity.runId)} state=${displayValue(identity.runState)}`,
|
||||
` Command: ${displayValue(identity.commandId)} state=${displayValue(identity.commandState)}`,
|
||||
` Runner: ${displayValue(identity.runnerJobId)} state=${displayValue(identity.runnerState)}`,
|
||||
` Session: ${displayValue(identity.sessionId)} state=${displayValue(identity.sessionState)}`,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
type TargetDispatchDecision = "dispatched" | "idempotent-replay" | "concurrent-replay";
|
||||
|
||||
export function compactTargetTaskMutationPayload(raw: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const data = asRecord(innerData(raw));
|
||||
const targetTask = firstRecord(data.targetTask, raw.targetTask);
|
||||
const aipodBinding = firstRecord(data.aipodBinding, raw.aipodBinding);
|
||||
const autoDispatch = firstRecord(data.autoDispatch, raw.autoDispatch);
|
||||
const decision = boundedText(autoDispatch.decision, 80);
|
||||
if (Object.keys(targetTask).length === 0 || !isTargetDispatchDecision(decision)) return null;
|
||||
|
||||
const identity = compactTargetTaskIdentity(data, autoDispatch);
|
||||
const targetSummary = compactTargetTaskSummary(targetTask);
|
||||
const aipodSummary = compactTargetAipodBinding(aipodBinding);
|
||||
const autoDispatchSummary = {
|
||||
enabled: autoDispatch.enabled === true,
|
||||
decision,
|
||||
mutation: booleanOrNull(autoDispatch.mutation),
|
||||
duplicateDispatchPrevented: booleanOrNull(autoDispatch.duplicateDispatchPrevented),
|
||||
identity,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
const next = targetTaskIdentityNext(identity, targetSummary, aipodSummary);
|
||||
|
||||
return {
|
||||
ok: raw.ok !== false,
|
||||
action: boundedText(raw.action, 120) ?? "queue-submit-auto-dispatch",
|
||||
data: {
|
||||
...identity,
|
||||
state: identity.taskState,
|
||||
mutation: booleanOrNull(data.mutation) ?? autoDispatchSummary.mutation,
|
||||
autoDispatch: autoDispatchSummary,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
targetTask: targetSummary,
|
||||
aipodBinding: aipodSummary,
|
||||
autoDispatch: autoDispatchSummary,
|
||||
next,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactTargetTaskIdentity(
|
||||
data: Record<string, unknown>,
|
||||
autoDispatch: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const declared = asRecord(autoDispatch.identity);
|
||||
const task = asRecord(data.task);
|
||||
const attempt = firstRecord(data.latestAttempt, task.latestAttempt);
|
||||
const run = asRecord(data.run);
|
||||
const command = asRecord(data.command);
|
||||
const runner = firstRecord(data.runner, data.runnerJob);
|
||||
const session = firstRecord(data.session, run.sessionRef, task.sessionRef);
|
||||
return {
|
||||
taskId: firstText(200, declared.taskId, data.taskId, task.id),
|
||||
taskState: firstText(80, declared.taskState, data.taskState, task.state, task.status),
|
||||
attemptId: firstText(200, declared.attemptId, data.attemptId, attempt.attemptId, attempt.id),
|
||||
attemptState: firstText(80, data.attemptState, attempt.state, attempt.status),
|
||||
runId: firstText(200, declared.runId, data.runId, run.id),
|
||||
runState: firstText(80, data.runState, run.state, run.status),
|
||||
commandId: firstText(200, declared.commandId, data.commandId, command.id),
|
||||
commandState: firstText(80, data.commandState, command.state, command.status),
|
||||
runnerJobId: firstText(200, declared.runnerJobId, data.runnerJobId, attempt.runnerJobId, runner.id),
|
||||
runnerState: firstText(80, data.runnerState, attempt.runnerState, runner.state, runner.status),
|
||||
sessionId: firstText(240, declared.sessionId, data.sessionId, attempt.sessionId, session.sessionId, session.id),
|
||||
sessionState: firstText(80, data.sessionState, session.state, session.status),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactTargetTaskSummary(targetTask: Record<string, unknown>): Record<string, unknown> {
|
||||
const workspace = asRecord(targetTask.workspace);
|
||||
return {
|
||||
mdtodoId: boundedText(targetTask.mdtodoId, 80),
|
||||
target: boundedText(targetTask.target, 120),
|
||||
targetWorkspace: boundedText(targetTask.targetWorkspace, 360),
|
||||
targetRoute: boundedText(targetTask.targetRoute, 480),
|
||||
projectId: boundedText(targetTask.projectId, 240),
|
||||
repository: boundedText(targetTask.repository, 240),
|
||||
ref: boundedText(targetTask.ref, 240),
|
||||
verifiedCommit: boundedText(targetTask.verifiedCommit, 160),
|
||||
refRelation: boundedText(targetTask.refRelation, 120),
|
||||
workspace: {
|
||||
branch: boundedText(workspace.branch, 240),
|
||||
headCommit: boundedText(workspace.headCommit, 160),
|
||||
clean: booleanOrNull(workspace.clean),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactTargetAipodBinding(binding: Record<string, unknown>): Record<string, unknown> {
|
||||
const workspaceRef = asRecord(binding.workspaceRef);
|
||||
const resourceBundleRef = asRecord(binding.resourceBundleRef);
|
||||
const session = asRecord(binding.session);
|
||||
const provider = compactCredentialPresence(binding.providerCredentials, "provider", 4);
|
||||
const tool = compactCredentialPresence(binding.toolCredentials, "tool", 6);
|
||||
return {
|
||||
aipod: boundedText(binding.aipod, 160),
|
||||
node: boundedText(binding.node, 120),
|
||||
lane: boundedText(binding.lane, 120),
|
||||
namespace: boundedText(binding.namespace, 160),
|
||||
projectId: boundedText(binding.projectId, 240),
|
||||
providerId: boundedText(binding.providerId, 160),
|
||||
backendProfile: boundedText(binding.backendProfile, 160),
|
||||
workspaceRef: {
|
||||
kind: boundedText(workspaceRef.kind, 80),
|
||||
path: boundedText(workspaceRef.path, 360),
|
||||
valid: booleanOrNull(workspaceRef.valid),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
resourceBundleRef: {
|
||||
kind: boundedText(resourceBundleRef.kind, 80),
|
||||
repoUrl: boundedRepositoryUrl(resourceBundleRef.repoUrl),
|
||||
ref: boundedText(resourceBundleRef.ref, 240),
|
||||
inheritedFromAipod: booleanOrNull(resourceBundleRef.inheritedFromAipod),
|
||||
credentialRefPresent: booleanOrNull(resourceBundleRef.credentialRefPresent),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
session: {
|
||||
sessionId: boundedText(session.sessionId, 240),
|
||||
identitySource: boundedText(session.identitySource, 120),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
credentialRefs: {
|
||||
totalCount: provider.count + tool.count,
|
||||
allPresent: provider.missingCount + tool.missingCount === 0,
|
||||
provider,
|
||||
tool,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactCredentialPresence(value: unknown, kind: "provider" | "tool", limit: number): {
|
||||
count: number;
|
||||
presentCount: number;
|
||||
missingCount: number;
|
||||
omittedCount: number;
|
||||
items: Record<string, unknown>[];
|
||||
valuesPrinted: false;
|
||||
} {
|
||||
const source = asRecords(value);
|
||||
const presentCount = source.filter((item) => item.bindingPresent === true).length;
|
||||
const items = source.slice(0, limit).map((item) => {
|
||||
const secretRef = asRecord(item.secretRef);
|
||||
return {
|
||||
...(kind === "provider"
|
||||
? { profile: boundedText(item.profile, 160) }
|
||||
: {
|
||||
tool: boundedText(item.tool, 160),
|
||||
purpose: boundedText(item.purpose, 200),
|
||||
}),
|
||||
secretRef: {
|
||||
namespace: boundedText(secretRef.namespace, 160),
|
||||
name: boundedText(secretRef.name, 200),
|
||||
keys: boundedStrings(secretRef.keys, 6, 80),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
bindingPresent: item.bindingPresent === true,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
count: source.length,
|
||||
presentCount,
|
||||
missingCount: source.length - presentCount,
|
||||
omittedCount: source.length - items.length,
|
||||
items,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function targetTaskIdentityNext(
|
||||
identity: Record<string, unknown>,
|
||||
targetTask: Record<string, unknown>,
|
||||
aipodBinding: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const taskId = asString(identity.taskId);
|
||||
const runId = asString(identity.runId);
|
||||
const commandId = asString(identity.commandId);
|
||||
const runnerJobId = asString(identity.runnerJobId);
|
||||
const sessionId = asString(identity.sessionId);
|
||||
const target = asString(targetTask.target) ?? asString(aipodBinding.node);
|
||||
const lane = asString(aipodBinding.lane);
|
||||
const suffix = [
|
||||
target === null ? null : `--target ${target}`,
|
||||
lane === null ? null : `--lane ${lane}`,
|
||||
].filter((value): value is string => value !== null).join(" ");
|
||||
const scoped = (command: string): string => suffix.length === 0 ? command : `${command} ${suffix}`;
|
||||
return {
|
||||
...(taskId === null ? {} : {
|
||||
task: scoped(`bun scripts/cli.ts agentrun describe task/${taskId}`),
|
||||
attempts: scoped(`bun scripts/cli.ts agentrun get attempts --task ${taskId}`),
|
||||
}),
|
||||
...(runId === null ? {} : {
|
||||
events: scoped(`bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 20`),
|
||||
}),
|
||||
...(runId === null || commandId === null ? {} : {
|
||||
result: scoped(`bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`),
|
||||
}),
|
||||
...(runnerJobId === null || runId === null ? {} : {
|
||||
runner: scoped(`bun scripts/cli.ts agentrun describe runnerjob/${runnerJobId} --run ${runId}`),
|
||||
}),
|
||||
...(sessionId === null ? {} : {
|
||||
logs: scoped(`bun scripts/cli.ts agentrun logs session/${sessionId} --tail 120`),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function innerData(value: Record<string, unknown>): unknown {
|
||||
return value.data ?? value;
|
||||
}
|
||||
|
||||
function firstRecord(...values: unknown[]): Record<string, unknown> {
|
||||
for (const value of values) {
|
||||
const item = asRecord(value);
|
||||
if (Object.keys(item).length > 0) return item;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function asRecords(value: unknown): Record<string, unknown>[] {
|
||||
return Array.isArray(value) ? value.map(asRecord).filter((item) => Object.keys(item).length > 0) : [];
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function firstText(maxChars: number, ...values: unknown[]): string | null {
|
||||
for (const value of values) {
|
||||
const text = boundedText(value, maxChars);
|
||||
if (text !== null) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maxChars: number): string | null {
|
||||
const text = asString(value);
|
||||
if (text === null) return null;
|
||||
const oneLine = text.replace(/\s+/gu, " ").trim();
|
||||
return oneLine.length <= maxChars ? oneLine : `${oneLine.slice(0, Math.max(0, maxChars - 1))}…`;
|
||||
}
|
||||
|
||||
function boundedStrings(value: unknown, limit: number, maxChars: number): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.slice(0, limit).map((item) => boundedText(item, maxChars)).filter((item): item is string => item !== null)
|
||||
: [];
|
||||
}
|
||||
|
||||
function boundedRepositoryUrl(value: unknown): string | null {
|
||||
const text = asString(value);
|
||||
if (text === null) return null;
|
||||
return boundedText(text.replace(/^(https?:\/\/)[^@/]+@/u, "$1[redacted]@"), 320);
|
||||
}
|
||||
|
||||
function booleanOrNull(value: unknown): boolean | null {
|
||||
return typeof value === "boolean" ? value : null;
|
||||
}
|
||||
|
||||
function isTargetDispatchDecision(value: string | null): value is TargetDispatchDecision {
|
||||
return value === "dispatched" || value === "idempotent-replay" || value === "concurrent-replay";
|
||||
}
|
||||
Reference in New Issue
Block a user