feat(agent): add controlled task execution

This commit is contained in:
2026-09-24 04:19:02 +08:00
parent d018bcae67
commit b31033e9ce
48 changed files with 14498 additions and 86 deletions
@@ -0,0 +1,28 @@
import { tool } from "@opencode-ai/plugin";
import { getInternalBridgeConfig } from "../internalBridge.js";
export default tool({
description: "Record information explicitly supplied or confirmed by the current user as human_input evidence for the current task. Only use in task collaboration. This requeues analysis; do not use it for Agent inference or tool-derived data.",
args: {
request_id: tool.schema.string().describe("Stable UUID idempotency key"),
evidence: tool.schema.object({
evidence_id: tool.schema.string(),
kind: tool.schema.literal("human_input"),
summary: tool.schema.string(),
source_label: tool.schema.string(),
source_ref: tool.schema.string().optional(),
observed_at: tool.schema.string(),
}),
},
async execute(command, context) {
const { baseUrl, internalToken } = getInternalBridgeConfig();
const response = await fetch(`${baseUrl}/internal/operational-task/human-evidence`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken },
body: JSON.stringify({ session_id: context.sessionID, command }),
});
const text = await response.text();
if (!response.ok) throw new Error(`human evidence publish failed (${response.status}): ${text}`);
return text;
},
});
+3 -4
View File
@@ -1,3 +1,5 @@
import { getInternalBridgeConfig } from "../internalBridge.js";
type FrontendActionArgs = Record<string, unknown> & {
__frontend_action_call_id?: string;
reason?: string;
@@ -10,15 +12,12 @@ type FrontendActionBridgeResult = {
error?: { code?: string; message?: string };
};
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
export const executeFrontendAction = async (
name: "render_scada_analysis" | "clear_scada_analysis",
args: FrontendActionArgs,
context: { sessionID: string },
) => {
const { baseUrl: internalBaseUrl, internalToken } = getInternalBridgeConfig();
const callId = args.__frontend_action_call_id;
if (!callId) {
throw new Error("frontend action bridge did not inject the OpenCode call ID");
@@ -0,0 +1,18 @@
import { tool } from "@opencode-ai/plugin";
import { getInternalBridgeConfig } from "../internalBridge.js";
export default tool({
description: "Read the current operational task's authorized task, plan, report, evidence and scoped investigation data from TJWater Server. Use this before investigating a task. It is read-only and does not expose credentials.",
args: {},
async execute(_args, context) {
const { baseUrl, internalToken } = getInternalBridgeConfig();
const url = new URL("/internal/operational-task/context", baseUrl);
url.searchParams.set("session_id", context.sessionID);
const response = await fetch(url, {
headers: { Accept: "application/json", "x-agent-internal-token": internalToken },
});
const text = await response.text();
if (!response.ok) throw new Error(`task context request failed (${response.status}): ${text}`);
return text;
},
});
+48
View File
@@ -0,0 +1,48 @@
import { tool } from "@opencode-ai/plugin";
import { getInternalBridgeConfig } from "../internalBridge.js";
import { proposedTaskActionSchema } from "../../src/product/operational-task-contract.js";
export default tool({
description: "Propose a work-order creation or command action for human approval after its node exists in the published current plan. This tool only creates a pending approval record and can never execute the action. After success, republish the plan with the returned action_id in action_refs before publishing the final report. create_work_order requires title, description and work_order_type; its priority is normal or urgent. issue_command requires command_type, target, parameters, impact_summary, non-empty preconditions and expires_at.",
args: {
request_id: tool.schema.string().describe("Stable UUID idempotency key for this proposal"),
kind: tool.schema.enum(["create_work_order", "issue_command"]),
reason: tool.schema.string(),
payload: tool.schema.union([
tool.schema.object({
title: tool.schema.string(),
description: tool.schema.string(),
work_order_type: tool.schema.string(),
priority: tool.schema.enum(["normal", "urgent"]).default("normal"),
scheduled_at: tool.schema.string().nullable().optional(),
location: tool.schema.string().nullable().optional(),
assignee: tool.schema.string().nullable().optional(),
}).strict(),
tool.schema.object({
command_type: tool.schema.string(),
target: tool.schema.object({
kind: tool.schema.string(),
id: tool.schema.string(),
}).strict(),
parameters: tool.schema.record(tool.schema.string(), tool.schema.unknown()),
impact_summary: tool.schema.string(),
preconditions: tool.schema.array(tool.schema.string()).min(1),
expires_at: tool.schema.string(),
}).strict(),
]),
evidence_refs: tool.schema.array(tool.schema.string()),
node_id: tool.schema.string().optional(),
},
async execute(action, context) {
const validatedAction = proposedTaskActionSchema.parse(action);
const { baseUrl, internalToken } = getInternalBridgeConfig();
const response = await fetch(`${baseUrl}/internal/operational-task/actions`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken },
body: JSON.stringify({ session_id: context.sessionID, action: validatedAction }),
});
const text = await response.text();
if (!response.ok) throw new Error(`task action proposal failed (${response.status}): ${text}`);
return text;
},
});
+37
View File
@@ -0,0 +1,37 @@
import { tool } from "@opencode-ai/plugin";
import { getInternalBridgeConfig } from "../internalBridge.js";
import { executionPlanSchema } from "../../src/routes/executionPlan.js";
export default tool({
description: "Publish the current full execution plan and progress to TJWater Server. Plans may be updated multiple times during investigation; a plan-only publish is intermediate and does not finish the task unless it explicitly records Agent failure. Publish the relevant plan node before proposing an action, then republish the plan with the returned action_id before the final report. Preserve stable node IDs and completed evidence. A running or completed node may only depend on completed or skipped predecessors. Edges are dependencies or labeled conditional branches, never guessed from list order. Human actions stay waiting until externally confirmed. Evidence must identify synthetic inputs, tool results or human confirmation accurately. This records a plan, it does not execute actions.",
args: {
request_id: tool.schema.string().describe("Stable UUID idempotency key for this plan revision"),
title: tool.schema.string(),
reason: tool.schema.string().describe("Why the plan or node status changed"),
nodes: tool.schema.array(tool.schema.object({
id: tool.schema.string(), title: tool.schema.string(),
status: tool.schema.enum(["pending", "running", "waiting", "completed", "failed", "skipped", "cancelled"]),
owner: tool.schema.enum(["agent", "human"]),
detail: tool.schema.string(), evidence: tool.schema.array(tool.schema.string()),
action_refs: tool.schema.array(tool.schema.string()).optional(),
})),
edges: tool.schema.array(tool.schema.object({ from: tool.schema.string(), to: tool.schema.string(), label: tool.schema.string() })),
},
async execute(args, context) {
const { request_id, ...input } = args;
const plan = executionPlanSchema.parse(input);
const { baseUrl, internalToken } = getInternalBridgeConfig();
const response = await fetch(`${baseUrl}/internal/operational-task/artifacts`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken },
body: JSON.stringify({
session_id: context.sessionID,
request_id,
execution_plan: plan,
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`task plan publish failed (${response.status}): ${text}`);
return text;
},
});
+36
View File
@@ -0,0 +1,36 @@
import { tool } from "@opencode-ai/plugin";
import { getInternalBridgeConfig } from "../internalBridge.js";
export default tool({
description: "Publish the final concise report for this execution turn after the current full plan and any pending action proposals have been saved. The Server combines this report with the current generation and session plan to settle the task as running, waiting_human or completed. This records artifacts; it does not perform field work or execute commands.",
args: {
request_id: tool.schema.string().describe("Stable UUID idempotency key for this report revision"),
report: tool.schema.object({
title: tool.schema.string(),
current_conclusion: tool.schema.string(),
conclusion_boundary: tool.schema.string(),
evidence_refs: tool.schema.array(tool.schema.string()),
recommendations: tool.schema.array(tool.schema.string()),
pending_items: tool.schema.array(tool.schema.string()),
}),
evidence: tool.schema.array(tool.schema.object({
evidence_id: tool.schema.string(),
kind: tool.schema.literal("synthetic"),
summary: tool.schema.string(),
source_label: tool.schema.string(),
source_ref: tool.schema.string().optional(),
observed_at: tool.schema.string(),
})),
},
async execute(args, context) {
const { baseUrl, internalToken } = getInternalBridgeConfig();
const response = await fetch(`${baseUrl}/internal/operational-task/artifacts`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken },
body: JSON.stringify({ session_id: context.sessionID, ...args }),
});
const text = await response.text();
if (!response.ok) throw new Error(`task report publish failed (${response.status}): ${text}`);
return text;
},
});
@@ -0,0 +1,32 @@
import { tool } from "@opencode-ai/plugin";
import { getInternalBridgeConfig } from "../internalBridge.js";
export default tool({
description: "Query one authorized operational data source through TJWater Server. Sources are finite and task-scoped; arbitrary URLs are not supported. peripheral_work_orders contains work-order and complaint business records. peripheral_observations is point/time-window monitoring observations, not complaint text, and requires point_id, start and end. Treat origin=synthetic as synthetic evidence. If truncated=true, do not infer that the unreturned full range has no anomalies.",
args: {
source: tool.schema.enum([
"workbench_runtime",
"scada_devices",
"scada_readings",
"peripheral_points",
"peripheral_work_orders",
"peripheral_observations",
"peripheral_test_data",
]),
device_ids: tool.schema.array(tool.schema.string()).optional(),
point_id: tool.schema.string().optional(),
start: tool.schema.string().optional(),
end: tool.schema.string().optional(),
},
async execute(query, context) {
const { baseUrl, internalToken } = getInternalBridgeConfig();
const response = await fetch(`${baseUrl}/internal/operational-task/data-query`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken },
body: JSON.stringify({ session_id: context.sessionID, query }),
});
const text = await response.text();
if (!response.ok) throw new Error(`task data query failed (${response.status}): ${text}`);
return text;
},
});
+2 -4
View File
@@ -1,8 +1,5 @@
import { tool } from "@opencode-ai/plugin";
const internalBaseUrl =
process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787";
const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
import { getInternalBridgeConfig } from "../internalBridge.js";
export default tool({
description:
@@ -22,6 +19,7 @@ export default tool({
.describe("Optional maximum number of hits to return."),
},
async execute(args, context) {
const { baseUrl: internalBaseUrl, internalToken } = getInternalBridgeConfig();
const response = await fetch(
`${internalBaseUrl}/internal/tools/session-search`,
{