@openlucaskaka/kagent 0.1.7
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 +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- package/src/kagent/utils/paths.py +473 -0
|
@@ -0,0 +1,3532 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import ipaddress
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Callable, Dict
|
|
17
|
+
|
|
18
|
+
from kagent.integrations.memory import MilvusLongTermMemory, RedisShortTermMemory
|
|
19
|
+
from kagent.providers.embeddings import (
|
|
20
|
+
EmbeddingProviderConfig,
|
|
21
|
+
OpenAICompatibleEmbeddingProvider,
|
|
22
|
+
)
|
|
23
|
+
from kagent.runtime.file_transaction import capture_text_states, workspace_transaction
|
|
24
|
+
from kagent.runtime.patch_checkpoints import (
|
|
25
|
+
PatchCheckpointStore,
|
|
26
|
+
commit_checkpointed_text_changes,
|
|
27
|
+
)
|
|
28
|
+
from kagent.runtime.policy import RuntimePolicy
|
|
29
|
+
from kagent.runtime.redaction import redact_runtime_payload
|
|
30
|
+
from kagent.runtime.sandbox import run_shell_sandboxed
|
|
31
|
+
from kagent.runtime.skills import RuntimeSkillRegistry
|
|
32
|
+
from kagent.runtime.task_state import TASK_EVENTS, TASK_STATES, TaskStateMachine
|
|
33
|
+
from kagent.runtime.types import AgentObservation
|
|
34
|
+
from kagent.runtime.workspace import VIRTUAL_WORKSPACE_KINDS, RuntimeWorkspace
|
|
35
|
+
|
|
36
|
+
RuntimeToolHandler = Callable[[Dict[str, Any]], Dict[str, Any]]
|
|
37
|
+
_ARTIFACT_KINDS = ("report", "plan", "decision", "data", "message")
|
|
38
|
+
_ARTIFACT_FORMATS = ("markdown", "plain_text", "json")
|
|
39
|
+
_TASK_STATUSES = ("pending", "in_progress", "blocked", "done", "failed")
|
|
40
|
+
_TASK_PRIORITIES = ("low", "normal", "high")
|
|
41
|
+
_SHORT_TEXT_MAX_LENGTH = 200
|
|
42
|
+
_TASK_TITLE_MAX_LENGTH = 500
|
|
43
|
+
_LONG_TEXT_MAX_LENGTH = 20000
|
|
44
|
+
_TAGS_MAX_ITEMS = 20
|
|
45
|
+
_DECISION_CRITERIA_MAX_ITEMS = 20
|
|
46
|
+
_DECISION_OPTIONS_MAX_ITEMS = 50
|
|
47
|
+
_RUBRIC_CRITERIA_MAX_ITEMS = 100
|
|
48
|
+
_TASK_ITEMS_MAX_ITEMS = 200
|
|
49
|
+
_RUBRIC_SEVERITIES = ("low", "normal", "blocking")
|
|
50
|
+
_DELEGATE_GOAL_MAX_LENGTH = 4000
|
|
51
|
+
_DELEGATE_MAX_ITERATIONS = 3
|
|
52
|
+
_HTTP_REQUEST_MAX_BYTES = 65536
|
|
53
|
+
_HTTP_REQUEST_TIMEOUT_SECONDS = 10.0
|
|
54
|
+
_BLOCKED_HTTP_HOSTS = {"localhost", "localhost."}
|
|
55
|
+
_HTTP_REDIRECT_STATUS_CODES = {301, 302, 303, 307, 308}
|
|
56
|
+
_URL_SECRET_KEY_PARTS = (
|
|
57
|
+
"api_key",
|
|
58
|
+
"apikey",
|
|
59
|
+
"authorization",
|
|
60
|
+
"bearer",
|
|
61
|
+
"password",
|
|
62
|
+
"secret",
|
|
63
|
+
"token",
|
|
64
|
+
)
|
|
65
|
+
_URL_SECRET_VALUE_PATTERNS = (
|
|
66
|
+
re.compile(r"sk-[A-Za-z0-9:_-]{6,}"),
|
|
67
|
+
re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._:/+=-]{6,}"),
|
|
68
|
+
)
|
|
69
|
+
_APPLY_PATCH_MAX_BYTES = 20000
|
|
70
|
+
_READ_FILE_MAX_BYTES = 65536
|
|
71
|
+
_LIST_FILES_MAX_DEPTH = 5
|
|
72
|
+
_LIST_FILES_MAX_ENTRIES = 500
|
|
73
|
+
_SHELL_COMMAND_MAX_LENGTH = 2000
|
|
74
|
+
_SHELL_COMMAND_MAX_OUTPUT_BYTES = 32768
|
|
75
|
+
_SHELL_COMMAND_DEFAULT_TIMEOUT_SECONDS = 10.0
|
|
76
|
+
_SHELL_COMMAND_MAX_TIMEOUT_SECONDS = 30.0
|
|
77
|
+
_SHELL_BACKGROUND_PATTERN = re.compile(r"(?<!&)&(?!&)")
|
|
78
|
+
_SHELL_INTERACTIVE_PATTERNS = (
|
|
79
|
+
re.compile(r"\bpython[0-9.]*\s+-i\b"),
|
|
80
|
+
re.compile(r"\b(ipython|node|ruby|irb)\b\s*$"),
|
|
81
|
+
re.compile(r"\b(read|vim|vi|nano|less|more|top|tail\s+-f)\b"),
|
|
82
|
+
)
|
|
83
|
+
_SHELL_DESTRUCTIVE_PATTERNS = (
|
|
84
|
+
re.compile(r"\brm\b(?=[^;&|]*-[A-Za-z]*r)(?=[^;&|]*-[A-Za-z]*f)"),
|
|
85
|
+
re.compile(r"\bsudo\b"),
|
|
86
|
+
re.compile(r"\b(?:chmod|chown|chgrp)\b(?=[^;&|]*\s-R\b)"),
|
|
87
|
+
re.compile(r"\bdd\s+if="),
|
|
88
|
+
re.compile(r"\b(?:mkfs|diskutil|shutdown|reboot|halt)\b"),
|
|
89
|
+
)
|
|
90
|
+
_SHELL_SECRET_EXPOSURE_PATTERNS = (
|
|
91
|
+
re.compile(r"^\s*(?:env|printenv|set|export)\b(?:\s|$)"),
|
|
92
|
+
re.compile(r"\b(?:cat|grep|awk|sed)\b[^;&|]*(?:^|[\s/\\])\.env(?:[\s.]|$)"),
|
|
93
|
+
re.compile(r"\b(?:cat|grep|awk|sed)\b[^;&|]*(?:secret|token|api[_-]?key)", re.I),
|
|
94
|
+
)
|
|
95
|
+
_SHELL_NETWORK_COMMAND_PATTERNS = (
|
|
96
|
+
re.compile(r"\b(?:curl|wget|ssh|scp|sftp|rsync|nc|netcat|telnet)\b"),
|
|
97
|
+
)
|
|
98
|
+
_SHELL_INLINE_NETWORK_CODE_PATTERNS = (
|
|
99
|
+
re.compile(
|
|
100
|
+
r"\bpython[0-9.]*\b[^;&|]*\s+-c\s+.+"
|
|
101
|
+
r"\b(?:socket|urllib|http\.client|requests|httpx|aiohttp)\b",
|
|
102
|
+
re.I,
|
|
103
|
+
),
|
|
104
|
+
re.compile(
|
|
105
|
+
r"\bnode\b[^;&|]*\s+-e\s+.+"
|
|
106
|
+
r"(?:\bfetch\s*\(|require\s*\(\s*['\"](?:net|http|https|dgram)['\"]\s*\))",
|
|
107
|
+
re.I,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
_SHELL_PIPE_TO_SHELL_PATTERN = re.compile(
|
|
111
|
+
r"\|\s*(?:sh|bash|zsh|python[0-9.]*|node|ruby)\b"
|
|
112
|
+
)
|
|
113
|
+
_RUNTIME_WORKSPACE_DIR_ENV_VARS = (
|
|
114
|
+
"KAGENT_RUNTIME_WORKSPACE_DIR",
|
|
115
|
+
"KAGENT_SERVICE_RUNTIME_WORKSPACE_DIR",
|
|
116
|
+
)
|
|
117
|
+
_RUNTIME_SKILLS_DIR_ENV_VARS = (
|
|
118
|
+
"KAGENT_RUNTIME_SKILLS_DIR",
|
|
119
|
+
"KAGENT_SKILLS_DIR",
|
|
120
|
+
)
|
|
121
|
+
_REDIS_URL_ENV_VARS = ("KAGENT_REDIS_URL",)
|
|
122
|
+
_MILVUS_URL_ENV_VARS = ("KAGENT_MILVUS_URL",)
|
|
123
|
+
_EMBEDDING_BASE_URL_ENV_VARS = ("KAGENT_EMBEDDING_BASE_URL", "KAGENT_LLM_BASE_URL")
|
|
124
|
+
_EMBEDDING_API_KEY_ENV_VARS = ("KAGENT_EMBEDDING_API_KEY", "KAGENT_LLM_API_KEY")
|
|
125
|
+
_EMBEDDING_MODEL_ENV_VARS = ("KAGENT_EMBEDDING_MODEL",)
|
|
126
|
+
_EMBEDDING_MAX_RETRIES_ENV_VAR = "KAGENT_EMBEDDING_MAX_RETRIES"
|
|
127
|
+
_EMBEDDING_RETRY_BACKOFF_ENV_VAR = "KAGENT_EMBEDDING_RETRY_BACKOFF_SECONDS"
|
|
128
|
+
_EXTERNAL_BACKEND_TIMEOUT_ENV_VAR = "KAGENT_EXTERNAL_BACKEND_TIMEOUT_SECONDS"
|
|
129
|
+
_APP_NAME_MAX_LENGTH = 120
|
|
130
|
+
_APP_NAME_ALLOWED_PATTERN = re.compile(r"^[\w .+()#&-]+$", re.UNICODE)
|
|
131
|
+
_OPEN_COMMAND_TIMEOUT_SECONDS = 10.0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class _PatchOperation:
|
|
136
|
+
operation: str
|
|
137
|
+
relative_path: str
|
|
138
|
+
destination_path: str = ""
|
|
139
|
+
content: str = ""
|
|
140
|
+
lines: tuple[str, ...] = ()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class RuntimeToolSpec:
|
|
145
|
+
name: str
|
|
146
|
+
description: str
|
|
147
|
+
handler: RuntimeToolHandler
|
|
148
|
+
input_schema: Dict[str, Any] = field(default_factory=lambda: {"type": "object"})
|
|
149
|
+
output_schema: Dict[str, Any] = field(default_factory=lambda: {"type": "object"})
|
|
150
|
+
timeout_seconds: float = 30.0
|
|
151
|
+
|
|
152
|
+
def __post_init__(self) -> None:
|
|
153
|
+
if self.timeout_seconds <= 0:
|
|
154
|
+
raise ValueError("timeout_seconds must be positive")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_TEXT_OUTPUT_SCHEMA = {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"required": ["text"],
|
|
160
|
+
"properties": {"text": {"type": "string"}},
|
|
161
|
+
"additionalProperties": False,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_ARTIFACT_OUTPUT_SCHEMA = {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"required": [
|
|
167
|
+
"artifact_id",
|
|
168
|
+
"title",
|
|
169
|
+
"kind",
|
|
170
|
+
"format",
|
|
171
|
+
"content",
|
|
172
|
+
"tags",
|
|
173
|
+
"bytes",
|
|
174
|
+
],
|
|
175
|
+
"properties": {
|
|
176
|
+
"artifact_id": {"type": "string"},
|
|
177
|
+
"title": {"type": "string"},
|
|
178
|
+
"kind": {"type": "string", "enum": list(_ARTIFACT_KINDS)},
|
|
179
|
+
"format": {"type": "string", "enum": list(_ARTIFACT_FORMATS)},
|
|
180
|
+
"content": {"type": "string"},
|
|
181
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
182
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
183
|
+
},
|
|
184
|
+
"additionalProperties": False,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_APPLY_PATCH_CHANGED_FILE_OUTPUT_SCHEMA = {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"required": ["path", "operation", "bytes", "sha256"],
|
|
190
|
+
"properties": {
|
|
191
|
+
"path": {"type": "string"},
|
|
192
|
+
"previous_path": {"type": "string"},
|
|
193
|
+
"operation": {"type": "string", "enum": ["add", "update", "delete", "move"]},
|
|
194
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
195
|
+
"sha256": {"type": "string"},
|
|
196
|
+
},
|
|
197
|
+
"additionalProperties": False,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_APPLY_PATCH_OUTPUT_SCHEMA = {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"required": ["changed_files", "file_count"],
|
|
203
|
+
"properties": {
|
|
204
|
+
"changed_files": {
|
|
205
|
+
"type": "array",
|
|
206
|
+
"items": _APPLY_PATCH_CHANGED_FILE_OUTPUT_SCHEMA,
|
|
207
|
+
},
|
|
208
|
+
"file_count": {"type": "number", "minimum": 0},
|
|
209
|
+
},
|
|
210
|
+
"additionalProperties": False,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_PATCH_HISTORY_OUTPUT_SCHEMA = {
|
|
214
|
+
"type": "object",
|
|
215
|
+
"required": ["checkpoints", "checkpoint_count"],
|
|
216
|
+
"properties": {
|
|
217
|
+
"checkpoints": {
|
|
218
|
+
"type": "array",
|
|
219
|
+
"items": {
|
|
220
|
+
"type": "object",
|
|
221
|
+
"required": ["checkpoint_id", "created_at", "file_count", "paths"],
|
|
222
|
+
"properties": {
|
|
223
|
+
"checkpoint_id": {"type": "string"},
|
|
224
|
+
"created_at": {"type": "string"},
|
|
225
|
+
"file_count": {"type": "number", "minimum": 0},
|
|
226
|
+
"paths": {"type": "array", "items": {"type": "string"}},
|
|
227
|
+
},
|
|
228
|
+
"additionalProperties": False,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
"checkpoint_count": {"type": "number", "minimum": 0},
|
|
232
|
+
},
|
|
233
|
+
"additionalProperties": False,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_REVERT_PATCH_OUTPUT_SCHEMA = {
|
|
237
|
+
"type": "object",
|
|
238
|
+
"required": [
|
|
239
|
+
"checkpoint_id",
|
|
240
|
+
"reverted_checkpoint_id",
|
|
241
|
+
"changed_files",
|
|
242
|
+
"file_count",
|
|
243
|
+
],
|
|
244
|
+
"properties": {
|
|
245
|
+
"checkpoint_id": {"type": "string"},
|
|
246
|
+
"reverted_checkpoint_id": {"type": "string"},
|
|
247
|
+
"changed_files": {
|
|
248
|
+
"type": "array",
|
|
249
|
+
"items": _APPLY_PATCH_CHANGED_FILE_OUTPUT_SCHEMA,
|
|
250
|
+
},
|
|
251
|
+
"file_count": {"type": "number", "minimum": 0},
|
|
252
|
+
},
|
|
253
|
+
"additionalProperties": False,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_DECISION_CRITERION_OUTPUT_SCHEMA = {
|
|
257
|
+
"type": "object",
|
|
258
|
+
"required": ["name", "weight"],
|
|
259
|
+
"properties": {
|
|
260
|
+
"name": {"type": "string"},
|
|
261
|
+
"weight": {"type": "number", "minimum": 0},
|
|
262
|
+
},
|
|
263
|
+
"additionalProperties": False,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
_DECISION_RANKING_OUTPUT_SCHEMA = {
|
|
267
|
+
"type": "object",
|
|
268
|
+
"required": ["rank", "name", "score", "scores", "rationale"],
|
|
269
|
+
"properties": {
|
|
270
|
+
"rank": {"type": "number", "minimum": 1},
|
|
271
|
+
"name": {"type": "string"},
|
|
272
|
+
"score": {"type": "number"},
|
|
273
|
+
"scores": {"type": "array", "items": {"type": "number"}},
|
|
274
|
+
"rationale": {"type": "string"},
|
|
275
|
+
},
|
|
276
|
+
"additionalProperties": False,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
_DECISION_MATRIX_OUTPUT_SCHEMA = {
|
|
280
|
+
"type": "object",
|
|
281
|
+
"required": ["question", "criteria", "rankings", "winner"],
|
|
282
|
+
"properties": {
|
|
283
|
+
"question": {"type": "string"},
|
|
284
|
+
"criteria": {
|
|
285
|
+
"type": "array",
|
|
286
|
+
"items": _DECISION_CRITERION_OUTPUT_SCHEMA,
|
|
287
|
+
},
|
|
288
|
+
"rankings": {
|
|
289
|
+
"type": "array",
|
|
290
|
+
"items": _DECISION_RANKING_OUTPUT_SCHEMA,
|
|
291
|
+
},
|
|
292
|
+
"winner": {"type": "string"},
|
|
293
|
+
},
|
|
294
|
+
"additionalProperties": False,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_RUBRIC_CRITERION_OUTPUT_SCHEMA = {
|
|
298
|
+
"type": "object",
|
|
299
|
+
"required": ["name", "passed", "severity", "evidence"],
|
|
300
|
+
"properties": {
|
|
301
|
+
"name": {"type": "string"},
|
|
302
|
+
"passed": {"type": "boolean"},
|
|
303
|
+
"severity": {"type": "string", "enum": list(_RUBRIC_SEVERITIES)},
|
|
304
|
+
"evidence": {"type": "string"},
|
|
305
|
+
},
|
|
306
|
+
"additionalProperties": False,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_RUBRIC_SCORE_OUTPUT_SCHEMA = {
|
|
310
|
+
"type": "object",
|
|
311
|
+
"required": [
|
|
312
|
+
"criteria",
|
|
313
|
+
"passed",
|
|
314
|
+
"failed",
|
|
315
|
+
"total",
|
|
316
|
+
"score_percent",
|
|
317
|
+
"blocking_failures",
|
|
318
|
+
"failed_criteria",
|
|
319
|
+
],
|
|
320
|
+
"properties": {
|
|
321
|
+
"criteria": {
|
|
322
|
+
"type": "array",
|
|
323
|
+
"items": _RUBRIC_CRITERION_OUTPUT_SCHEMA,
|
|
324
|
+
},
|
|
325
|
+
"passed": {"type": "number", "minimum": 0},
|
|
326
|
+
"failed": {"type": "number", "minimum": 0},
|
|
327
|
+
"total": {"type": "number", "minimum": 0},
|
|
328
|
+
"score_percent": {"type": "number", "minimum": 0, "maximum": 100},
|
|
329
|
+
"blocking_failures": {"type": "array", "items": {"type": "string"}},
|
|
330
|
+
"failed_criteria": {"type": "array", "items": {"type": "string"}},
|
|
331
|
+
},
|
|
332
|
+
"additionalProperties": False,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_TASK_ITEM_OUTPUT_SCHEMA = {
|
|
336
|
+
"type": "object",
|
|
337
|
+
"required": ["title", "status", "priority"],
|
|
338
|
+
"properties": {
|
|
339
|
+
"title": {"type": "string"},
|
|
340
|
+
"status": {"type": "string", "enum": list(_TASK_STATUSES)},
|
|
341
|
+
"priority": {"type": "string", "enum": list(_TASK_PRIORITIES)},
|
|
342
|
+
"owner": {"type": "string"},
|
|
343
|
+
"due": {"type": "string"},
|
|
344
|
+
},
|
|
345
|
+
"additionalProperties": False,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_TASK_STATUS_COUNTS_OUTPUT_SCHEMA = {
|
|
349
|
+
"type": "object",
|
|
350
|
+
"required": list(_TASK_STATUSES),
|
|
351
|
+
"properties": {
|
|
352
|
+
"pending": {"type": "number", "minimum": 0},
|
|
353
|
+
"in_progress": {"type": "number", "minimum": 0},
|
|
354
|
+
"blocked": {"type": "number", "minimum": 0},
|
|
355
|
+
"done": {"type": "number", "minimum": 0},
|
|
356
|
+
"failed": {"type": "number", "minimum": 0},
|
|
357
|
+
},
|
|
358
|
+
"additionalProperties": False,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
_TASK_LIST_OUTPUT_SCHEMA = {
|
|
362
|
+
"type": "object",
|
|
363
|
+
"required": ["items", "counts", "total"],
|
|
364
|
+
"properties": {
|
|
365
|
+
"items": {"type": "array", "items": _TASK_ITEM_OUTPUT_SCHEMA},
|
|
366
|
+
"counts": _TASK_STATUS_COUNTS_OUTPUT_SCHEMA,
|
|
367
|
+
"total": {"type": "number", "minimum": 0},
|
|
368
|
+
},
|
|
369
|
+
"additionalProperties": False,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_TASK_TRANSITION_OUTPUT_SCHEMA = {
|
|
373
|
+
"type": "object",
|
|
374
|
+
"required": ["previous_state", "event", "state"],
|
|
375
|
+
"properties": {
|
|
376
|
+
"previous_state": {"type": "string", "enum": list(TASK_STATES)},
|
|
377
|
+
"event": {"type": "string", "enum": list(TASK_EVENTS)},
|
|
378
|
+
"state": {"type": "string", "enum": list(TASK_STATES)},
|
|
379
|
+
},
|
|
380
|
+
"additionalProperties": False,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_HTTP_REQUEST_OUTPUT_SCHEMA = {
|
|
384
|
+
"type": "object",
|
|
385
|
+
"required": [
|
|
386
|
+
"url",
|
|
387
|
+
"status_code",
|
|
388
|
+
"content_type",
|
|
389
|
+
"body_text",
|
|
390
|
+
"bytes",
|
|
391
|
+
"truncated",
|
|
392
|
+
],
|
|
393
|
+
"properties": {
|
|
394
|
+
"url": {"type": "string"},
|
|
395
|
+
"status_code": {"type": "number", "minimum": 100, "maximum": 599},
|
|
396
|
+
"content_type": {"type": "string"},
|
|
397
|
+
"body_text": {"type": "string"},
|
|
398
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
399
|
+
"truncated": {"type": "boolean"},
|
|
400
|
+
},
|
|
401
|
+
"additionalProperties": False,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_OPEN_URL_OUTPUT_SCHEMA = {
|
|
405
|
+
"type": "object",
|
|
406
|
+
"required": ["url", "opened", "application", "command"],
|
|
407
|
+
"properties": {
|
|
408
|
+
"url": {"type": "string"},
|
|
409
|
+
"opened": {"type": "boolean"},
|
|
410
|
+
"application": {"type": "string"},
|
|
411
|
+
"command": {"type": "string"},
|
|
412
|
+
},
|
|
413
|
+
"additionalProperties": False,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_OPEN_APP_OUTPUT_SCHEMA = {
|
|
417
|
+
"type": "object",
|
|
418
|
+
"required": ["application", "opened", "command"],
|
|
419
|
+
"properties": {
|
|
420
|
+
"application": {"type": "string"},
|
|
421
|
+
"opened": {"type": "boolean"},
|
|
422
|
+
"command": {"type": "string"},
|
|
423
|
+
},
|
|
424
|
+
"additionalProperties": False,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
_READ_FILE_OUTPUT_SCHEMA = {
|
|
428
|
+
"type": "object",
|
|
429
|
+
"required": ["path", "content", "bytes", "truncated", "sha256"],
|
|
430
|
+
"properties": {
|
|
431
|
+
"path": {"type": "string"},
|
|
432
|
+
"content": {"type": "string"},
|
|
433
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
434
|
+
"truncated": {"type": "boolean"},
|
|
435
|
+
"sha256": {"type": "string"},
|
|
436
|
+
},
|
|
437
|
+
"additionalProperties": False,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
_LIST_FILES_ENTRY_OUTPUT_SCHEMA = {
|
|
441
|
+
"type": "object",
|
|
442
|
+
"required": ["path", "type", "bytes"],
|
|
443
|
+
"properties": {
|
|
444
|
+
"path": {"type": "string"},
|
|
445
|
+
"type": {"type": "string", "enum": ["directory", "file"]},
|
|
446
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
447
|
+
},
|
|
448
|
+
"additionalProperties": False,
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
_LIST_FILES_OUTPUT_SCHEMA = {
|
|
452
|
+
"type": "object",
|
|
453
|
+
"required": ["root", "entries", "file_count", "truncated"],
|
|
454
|
+
"properties": {
|
|
455
|
+
"root": {"type": "string"},
|
|
456
|
+
"entries": {"type": "array", "items": _LIST_FILES_ENTRY_OUTPUT_SCHEMA},
|
|
457
|
+
"file_count": {"type": "number", "minimum": 0},
|
|
458
|
+
"truncated": {"type": "boolean"},
|
|
459
|
+
},
|
|
460
|
+
"additionalProperties": False,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
_WORKSPACE_ASSET_METADATA_SCHEMA = {
|
|
464
|
+
"type": "object",
|
|
465
|
+
"required": [
|
|
466
|
+
"kind",
|
|
467
|
+
"path",
|
|
468
|
+
"bytes",
|
|
469
|
+
"sha256",
|
|
470
|
+
"created_at",
|
|
471
|
+
"updated_at",
|
|
472
|
+
"metadata",
|
|
473
|
+
],
|
|
474
|
+
"properties": {
|
|
475
|
+
"kind": {"type": "string", "enum": list(VIRTUAL_WORKSPACE_KINDS)},
|
|
476
|
+
"path": {"type": "string"},
|
|
477
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
478
|
+
"sha256": {"type": "string"},
|
|
479
|
+
"created_at": {"type": "string"},
|
|
480
|
+
"updated_at": {"type": "string"},
|
|
481
|
+
"metadata": {"type": "object"},
|
|
482
|
+
},
|
|
483
|
+
"additionalProperties": False,
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
_WORKSPACE_READ_OUTPUT_SCHEMA = {
|
|
487
|
+
"type": "object",
|
|
488
|
+
"required": [
|
|
489
|
+
"kind",
|
|
490
|
+
"path",
|
|
491
|
+
"bytes",
|
|
492
|
+
"sha256",
|
|
493
|
+
"created_at",
|
|
494
|
+
"updated_at",
|
|
495
|
+
"metadata",
|
|
496
|
+
"content",
|
|
497
|
+
"truncated",
|
|
498
|
+
],
|
|
499
|
+
"properties": {
|
|
500
|
+
**_WORKSPACE_ASSET_METADATA_SCHEMA["properties"],
|
|
501
|
+
"content": {"type": "string"},
|
|
502
|
+
"truncated": {"type": "boolean"},
|
|
503
|
+
},
|
|
504
|
+
"additionalProperties": False,
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
_WORKSPACE_LIST_ENTRY_OUTPUT_SCHEMA = {
|
|
508
|
+
"type": "object",
|
|
509
|
+
"required": ["path", "type", "bytes", "sha256"],
|
|
510
|
+
"properties": {
|
|
511
|
+
"path": {"type": "string"},
|
|
512
|
+
"type": {"type": "string", "enum": ["directory", "file"]},
|
|
513
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
514
|
+
"sha256": {"type": "string"},
|
|
515
|
+
},
|
|
516
|
+
"additionalProperties": False,
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_WORKSPACE_LIST_OUTPUT_SCHEMA = {
|
|
520
|
+
"type": "object",
|
|
521
|
+
"required": ["kind", "root", "entries", "file_count", "truncated"],
|
|
522
|
+
"properties": {
|
|
523
|
+
"kind": {"type": "string", "enum": list(VIRTUAL_WORKSPACE_KINDS)},
|
|
524
|
+
"root": {"type": "string"},
|
|
525
|
+
"entries": {
|
|
526
|
+
"type": "array",
|
|
527
|
+
"items": _WORKSPACE_LIST_ENTRY_OUTPUT_SCHEMA,
|
|
528
|
+
},
|
|
529
|
+
"file_count": {"type": "number", "minimum": 0},
|
|
530
|
+
"truncated": {"type": "boolean"},
|
|
531
|
+
},
|
|
532
|
+
"additionalProperties": False,
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
_WORKSPACE_SEARCH_MATCH_OUTPUT_SCHEMA = {
|
|
536
|
+
"type": "object",
|
|
537
|
+
"required": ["path", "line_number", "line", "byte_offset", "sha256"],
|
|
538
|
+
"properties": {
|
|
539
|
+
"path": {"type": "string"},
|
|
540
|
+
"line_number": {"type": "number", "minimum": 1},
|
|
541
|
+
"line": {"type": "string"},
|
|
542
|
+
"byte_offset": {"type": "number", "minimum": 0},
|
|
543
|
+
"sha256": {"type": "string"},
|
|
544
|
+
},
|
|
545
|
+
"additionalProperties": False,
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
_WORKSPACE_SEARCH_OUTPUT_SCHEMA = {
|
|
549
|
+
"type": "object",
|
|
550
|
+
"required": ["kind", "root", "query", "matches", "match_count", "truncated"],
|
|
551
|
+
"properties": {
|
|
552
|
+
"kind": {"type": "string", "enum": list(VIRTUAL_WORKSPACE_KINDS)},
|
|
553
|
+
"root": {"type": "string"},
|
|
554
|
+
"query": {"type": "string"},
|
|
555
|
+
"matches": {
|
|
556
|
+
"type": "array",
|
|
557
|
+
"items": _WORKSPACE_SEARCH_MATCH_OUTPUT_SCHEMA,
|
|
558
|
+
},
|
|
559
|
+
"match_count": {"type": "number", "minimum": 0},
|
|
560
|
+
"truncated": {"type": "boolean"},
|
|
561
|
+
},
|
|
562
|
+
"additionalProperties": False,
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
_WORKSPACE_HISTORY_REVISION_OUTPUT_SCHEMA = {
|
|
566
|
+
"type": "object",
|
|
567
|
+
"required": [
|
|
568
|
+
"revision_id",
|
|
569
|
+
"bytes",
|
|
570
|
+
"sha256",
|
|
571
|
+
"created_at",
|
|
572
|
+
"content",
|
|
573
|
+
"content_truncated",
|
|
574
|
+
],
|
|
575
|
+
"properties": {
|
|
576
|
+
"revision_id": {"type": "string"},
|
|
577
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
578
|
+
"sha256": {"type": "string"},
|
|
579
|
+
"created_at": {"type": "string"},
|
|
580
|
+
"content": {"type": "string"},
|
|
581
|
+
"content_truncated": {"type": "boolean"},
|
|
582
|
+
},
|
|
583
|
+
"additionalProperties": False,
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
_WORKSPACE_HISTORY_OUTPUT_SCHEMA = {
|
|
587
|
+
"type": "object",
|
|
588
|
+
"required": ["kind", "path", "revisions", "revision_count", "truncated"],
|
|
589
|
+
"properties": {
|
|
590
|
+
"kind": {"type": "string", "enum": list(VIRTUAL_WORKSPACE_KINDS)},
|
|
591
|
+
"path": {"type": "string"},
|
|
592
|
+
"revisions": {
|
|
593
|
+
"type": "array",
|
|
594
|
+
"items": _WORKSPACE_HISTORY_REVISION_OUTPUT_SCHEMA,
|
|
595
|
+
},
|
|
596
|
+
"revision_count": {"type": "number", "minimum": 0},
|
|
597
|
+
"truncated": {"type": "boolean"},
|
|
598
|
+
},
|
|
599
|
+
"additionalProperties": False,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
_WORKSPACE_DIFF_OUTPUT_SCHEMA = {
|
|
603
|
+
"type": "object",
|
|
604
|
+
"required": [
|
|
605
|
+
"kind",
|
|
606
|
+
"path",
|
|
607
|
+
"revision_id",
|
|
608
|
+
"from_sha256",
|
|
609
|
+
"to_sha256",
|
|
610
|
+
"diff",
|
|
611
|
+
"bytes",
|
|
612
|
+
"truncated",
|
|
613
|
+
],
|
|
614
|
+
"properties": {
|
|
615
|
+
"kind": {"type": "string", "enum": list(VIRTUAL_WORKSPACE_KINDS)},
|
|
616
|
+
"path": {"type": "string"},
|
|
617
|
+
"revision_id": {"type": "string"},
|
|
618
|
+
"from_sha256": {"type": "string"},
|
|
619
|
+
"to_sha256": {"type": "string"},
|
|
620
|
+
"diff": {"type": "string"},
|
|
621
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
622
|
+
"truncated": {"type": "boolean"},
|
|
623
|
+
},
|
|
624
|
+
"additionalProperties": False,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
_WORKSPACE_RESTORE_OUTPUT_SCHEMA = {
|
|
628
|
+
"type": "object",
|
|
629
|
+
"required": [
|
|
630
|
+
"kind",
|
|
631
|
+
"path",
|
|
632
|
+
"restored_revision_id",
|
|
633
|
+
"previous_sha256",
|
|
634
|
+
"sha256",
|
|
635
|
+
"bytes",
|
|
636
|
+
"updated_at",
|
|
637
|
+
],
|
|
638
|
+
"properties": {
|
|
639
|
+
"kind": {"type": "string", "enum": list(VIRTUAL_WORKSPACE_KINDS)},
|
|
640
|
+
"path": {"type": "string"},
|
|
641
|
+
"restored_revision_id": {"type": "string"},
|
|
642
|
+
"previous_sha256": {"type": "string"},
|
|
643
|
+
"sha256": {"type": "string"},
|
|
644
|
+
"bytes": {"type": "number", "minimum": 0},
|
|
645
|
+
"updated_at": {"type": "string"},
|
|
646
|
+
},
|
|
647
|
+
"additionalProperties": False,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
_MEMORY_PUT_OUTPUT_SCHEMA = {
|
|
651
|
+
"type": "object",
|
|
652
|
+
"required": ["backend", "namespace", "key", "stored", "ttl_seconds"],
|
|
653
|
+
"properties": {
|
|
654
|
+
"backend": {"type": "string", "enum": ["redis"]},
|
|
655
|
+
"namespace": {"type": "string"},
|
|
656
|
+
"key": {"type": "string"},
|
|
657
|
+
"stored": {"type": "boolean"},
|
|
658
|
+
"ttl_seconds": {"type": "string"},
|
|
659
|
+
},
|
|
660
|
+
"additionalProperties": False,
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
_MEMORY_GET_OUTPUT_SCHEMA = {
|
|
664
|
+
"type": "object",
|
|
665
|
+
"required": ["backend", "namespace", "key", "found", "value"],
|
|
666
|
+
"properties": {
|
|
667
|
+
"backend": {"type": "string", "enum": ["redis"]},
|
|
668
|
+
"namespace": {"type": "string"},
|
|
669
|
+
"key": {"type": "string"},
|
|
670
|
+
"found": {"type": "boolean"},
|
|
671
|
+
"value": {},
|
|
672
|
+
},
|
|
673
|
+
"additionalProperties": False,
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
_MEMORY_UPSERT_OUTPUT_SCHEMA = {
|
|
677
|
+
"type": "object",
|
|
678
|
+
"required": ["backend", "collection", "memory_id", "stored"],
|
|
679
|
+
"properties": {
|
|
680
|
+
"backend": {"type": "string", "enum": ["milvus"]},
|
|
681
|
+
"collection": {"type": "string"},
|
|
682
|
+
"memory_id": {"type": "string"},
|
|
683
|
+
"stored": {"type": "boolean"},
|
|
684
|
+
},
|
|
685
|
+
"additionalProperties": False,
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
_MEMORY_SEARCH_OUTPUT_SCHEMA = {
|
|
689
|
+
"type": "object",
|
|
690
|
+
"required": ["backend", "collection", "matches", "match_count"],
|
|
691
|
+
"properties": {
|
|
692
|
+
"backend": {"type": "string", "enum": ["milvus"]},
|
|
693
|
+
"collection": {"type": "string"},
|
|
694
|
+
"matches": {
|
|
695
|
+
"type": "array",
|
|
696
|
+
"items": {
|
|
697
|
+
"type": "object",
|
|
698
|
+
"required": ["memory_id", "text", "score", "metadata"],
|
|
699
|
+
"properties": {
|
|
700
|
+
"memory_id": {"type": "string"},
|
|
701
|
+
"text": {"type": "string"},
|
|
702
|
+
"score": {"type": "number"},
|
|
703
|
+
"metadata": {"type": "object"},
|
|
704
|
+
},
|
|
705
|
+
"additionalProperties": False,
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
"match_count": {"type": "number", "minimum": 0},
|
|
709
|
+
},
|
|
710
|
+
"additionalProperties": False,
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
_MEMORY_REMEMBER_OUTPUT_SCHEMA = {
|
|
714
|
+
"type": "object",
|
|
715
|
+
"required": [
|
|
716
|
+
"backend",
|
|
717
|
+
"collection",
|
|
718
|
+
"memory_id",
|
|
719
|
+
"stored",
|
|
720
|
+
"embedding_model",
|
|
721
|
+
"vector_dimensions",
|
|
722
|
+
],
|
|
723
|
+
"properties": {
|
|
724
|
+
**_MEMORY_UPSERT_OUTPUT_SCHEMA["properties"],
|
|
725
|
+
"embedding_model": {"type": "string"},
|
|
726
|
+
"vector_dimensions": {"type": "string"},
|
|
727
|
+
},
|
|
728
|
+
"additionalProperties": False,
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
_MEMORY_RECALL_OUTPUT_SCHEMA = {
|
|
732
|
+
"type": "object",
|
|
733
|
+
"required": [
|
|
734
|
+
"backend",
|
|
735
|
+
"collection",
|
|
736
|
+
"matches",
|
|
737
|
+
"match_count",
|
|
738
|
+
"embedding_model",
|
|
739
|
+
"vector_dimensions",
|
|
740
|
+
],
|
|
741
|
+
"properties": {
|
|
742
|
+
**_MEMORY_SEARCH_OUTPUT_SCHEMA["properties"],
|
|
743
|
+
"embedding_model": {"type": "string"},
|
|
744
|
+
"vector_dimensions": {"type": "string"},
|
|
745
|
+
},
|
|
746
|
+
"additionalProperties": False,
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
_DELEGATE_TASK_OUTPUT_SCHEMA = {
|
|
750
|
+
"type": "object",
|
|
751
|
+
"required": [
|
|
752
|
+
"child_run_id",
|
|
753
|
+
"status",
|
|
754
|
+
"answer",
|
|
755
|
+
"error_code",
|
|
756
|
+
"child_iteration_count",
|
|
757
|
+
"child_observation_count",
|
|
758
|
+
],
|
|
759
|
+
"properties": {
|
|
760
|
+
"child_run_id": {"type": "string"},
|
|
761
|
+
"status": {
|
|
762
|
+
"type": "string",
|
|
763
|
+
"enum": ["done", "failed", "requires_approval", "cancelled"],
|
|
764
|
+
},
|
|
765
|
+
"answer": {"type": "string"},
|
|
766
|
+
"error_code": {"type": "string"},
|
|
767
|
+
"child_iteration_count": {"type": "string"},
|
|
768
|
+
"child_observation_count": {"type": "string"},
|
|
769
|
+
},
|
|
770
|
+
"additionalProperties": False,
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
_SKILL_LIST_OUTPUT_SCHEMA = {
|
|
774
|
+
"type": "object",
|
|
775
|
+
"required": ["skills", "skill_count"],
|
|
776
|
+
"properties": {
|
|
777
|
+
"skills": {
|
|
778
|
+
"type": "array",
|
|
779
|
+
"items": {
|
|
780
|
+
"type": "object",
|
|
781
|
+
"required": ["name", "description", "tags"],
|
|
782
|
+
"properties": {
|
|
783
|
+
"name": {"type": "string"},
|
|
784
|
+
"description": {"type": "string"},
|
|
785
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
786
|
+
},
|
|
787
|
+
"additionalProperties": False,
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
"skill_count": {"type": "number", "minimum": 0},
|
|
791
|
+
},
|
|
792
|
+
"additionalProperties": False,
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
_SKILL_GET_OUTPUT_SCHEMA = {
|
|
796
|
+
"type": "object",
|
|
797
|
+
"required": ["name", "description", "instructions", "tags"],
|
|
798
|
+
"properties": {
|
|
799
|
+
"name": {"type": "string"},
|
|
800
|
+
"description": {"type": "string"},
|
|
801
|
+
"instructions": {"type": "string"},
|
|
802
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
803
|
+
},
|
|
804
|
+
"additionalProperties": False,
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
_SHELL_COMMAND_OUTPUT_SCHEMA = {
|
|
808
|
+
"type": "object",
|
|
809
|
+
"required": [
|
|
810
|
+
"command",
|
|
811
|
+
"cwd",
|
|
812
|
+
"sandbox",
|
|
813
|
+
"exit_code",
|
|
814
|
+
"stdout",
|
|
815
|
+
"stderr",
|
|
816
|
+
"duration_seconds",
|
|
817
|
+
"timed_out",
|
|
818
|
+
"truncated",
|
|
819
|
+
],
|
|
820
|
+
"properties": {
|
|
821
|
+
"command": {"type": "string"},
|
|
822
|
+
"cwd": {"type": "string"},
|
|
823
|
+
"sandbox": {
|
|
824
|
+
"type": "object",
|
|
825
|
+
"required": [
|
|
826
|
+
"enabled",
|
|
827
|
+
"backend",
|
|
828
|
+
"enforced",
|
|
829
|
+
"filesystem",
|
|
830
|
+
"network",
|
|
831
|
+
"env_policy",
|
|
832
|
+
],
|
|
833
|
+
"properties": {
|
|
834
|
+
"enabled": {"type": "string", "enum": ["true"]},
|
|
835
|
+
"backend": {
|
|
836
|
+
"type": "string",
|
|
837
|
+
"enum": ["linux-bwrap", "macos-seatbelt", "soft", "windows-soft"],
|
|
838
|
+
},
|
|
839
|
+
"enforced": {"type": "string", "enum": ["true", "false"]},
|
|
840
|
+
"filesystem": {"type": "string", "enum": ["workspace"]},
|
|
841
|
+
"network": {"type": "string", "enum": ["disabled"]},
|
|
842
|
+
"env_policy": {"type": "string", "enum": ["minimal"]},
|
|
843
|
+
"fallback_reason": {"type": "string"},
|
|
844
|
+
},
|
|
845
|
+
"additionalProperties": False,
|
|
846
|
+
},
|
|
847
|
+
"exit_code": {"type": "number"},
|
|
848
|
+
"stdout": {"type": "string"},
|
|
849
|
+
"stderr": {"type": "string"},
|
|
850
|
+
"duration_seconds": {"type": "number", "minimum": 0},
|
|
851
|
+
"timed_out": {"type": "boolean"},
|
|
852
|
+
"truncated": {"type": "boolean"},
|
|
853
|
+
},
|
|
854
|
+
"additionalProperties": False,
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def default_runtime_tools(
|
|
859
|
+
*,
|
|
860
|
+
runtime_workspace_dir: str = "",
|
|
861
|
+
runtime_skills_dir: str = "",
|
|
862
|
+
redis_url: str = "",
|
|
863
|
+
milvus_url: str = "",
|
|
864
|
+
embedding_base_url: str = "",
|
|
865
|
+
embedding_api_key: str = "",
|
|
866
|
+
embedding_model: str = "",
|
|
867
|
+
embedding_timeout_seconds: float = 30.0,
|
|
868
|
+
embedding_max_retries: int = 2,
|
|
869
|
+
embedding_retry_backoff_seconds: float = 0.25,
|
|
870
|
+
external_backend_timeout_seconds: float = 2.0,
|
|
871
|
+
delegate_runner: Callable[[str, int], Dict[str, Any]] | None = None,
|
|
872
|
+
include_delegate_tool: bool = True,
|
|
873
|
+
) -> Dict[str, RuntimeToolSpec]:
|
|
874
|
+
active_redis_url = redis_url.strip() or _first_env_value(_REDIS_URL_ENV_VARS)
|
|
875
|
+
active_milvus_url = milvus_url.strip() or _first_env_value(_MILVUS_URL_ENV_VARS)
|
|
876
|
+
active_embedding_config = _embedding_config(
|
|
877
|
+
base_url=embedding_base_url,
|
|
878
|
+
api_key=embedding_api_key,
|
|
879
|
+
model=embedding_model,
|
|
880
|
+
timeout_seconds=embedding_timeout_seconds,
|
|
881
|
+
max_retries=embedding_max_retries,
|
|
882
|
+
retry_backoff_seconds=embedding_retry_backoff_seconds,
|
|
883
|
+
)
|
|
884
|
+
active_backend_timeout = _backend_timeout_seconds(external_backend_timeout_seconds)
|
|
885
|
+
tools = {
|
|
886
|
+
"apply_patch": RuntimeToolSpec(
|
|
887
|
+
name="apply_patch",
|
|
888
|
+
description=(
|
|
889
|
+
"Apply a Codex-style workspace patch. Supports adding, updating, "
|
|
890
|
+
"moving, and deleting files "
|
|
891
|
+
"inside the current workspace and rejects absolute paths, parent "
|
|
892
|
+
"traversal, unsafe deletes, and accidental overwrites. To create hello.md, "
|
|
893
|
+
"use exactly: *** Begin Patch\n*** Add File: hello.md\n+content\n*** End Patch"
|
|
894
|
+
),
|
|
895
|
+
handler=_apply_patch,
|
|
896
|
+
input_schema={
|
|
897
|
+
"type": "object",
|
|
898
|
+
"required": ["patch"],
|
|
899
|
+
"properties": {
|
|
900
|
+
"patch": {
|
|
901
|
+
"type": "string",
|
|
902
|
+
"minLength": 1,
|
|
903
|
+
"maxLength": _APPLY_PATCH_MAX_BYTES,
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
"additionalProperties": False,
|
|
907
|
+
},
|
|
908
|
+
output_schema=_APPLY_PATCH_OUTPUT_SCHEMA,
|
|
909
|
+
),
|
|
910
|
+
"patch_history": RuntimeToolSpec(
|
|
911
|
+
name="patch_history",
|
|
912
|
+
description=(
|
|
913
|
+
"List committed file-change checkpoints for the current workspace. "
|
|
914
|
+
"Use this before revert_patch and pass back the exact checkpoint ID "
|
|
915
|
+
"and paths selected by the user."
|
|
916
|
+
),
|
|
917
|
+
handler=_patch_history,
|
|
918
|
+
input_schema={
|
|
919
|
+
"type": "object",
|
|
920
|
+
"properties": {
|
|
921
|
+
"limit": {"type": "number", "minimum": 1, "maximum": 100},
|
|
922
|
+
},
|
|
923
|
+
"additionalProperties": False,
|
|
924
|
+
},
|
|
925
|
+
output_schema=_PATCH_HISTORY_OUTPUT_SCHEMA,
|
|
926
|
+
),
|
|
927
|
+
"revert_patch": RuntimeToolSpec(
|
|
928
|
+
name="revert_patch",
|
|
929
|
+
description=(
|
|
930
|
+
"Restore files from a reviewed patch checkpoint. The exact paths from "
|
|
931
|
+
"patch_history are required and current file SHA-256 values must still "
|
|
932
|
+
"match the checkpoint. The revert creates a new checkpoint for redo."
|
|
933
|
+
),
|
|
934
|
+
handler=_revert_patch,
|
|
935
|
+
input_schema={
|
|
936
|
+
"type": "object",
|
|
937
|
+
"required": ["checkpoint_id", "paths"],
|
|
938
|
+
"properties": {
|
|
939
|
+
"checkpoint_id": {
|
|
940
|
+
"type": "string",
|
|
941
|
+
"minLength": 1,
|
|
942
|
+
"maxLength": 120,
|
|
943
|
+
},
|
|
944
|
+
"paths": {
|
|
945
|
+
"type": "array",
|
|
946
|
+
"minItems": 1,
|
|
947
|
+
"maxItems": 200,
|
|
948
|
+
"items": {"type": "string", "minLength": 1},
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
"additionalProperties": False,
|
|
952
|
+
},
|
|
953
|
+
output_schema=_REVERT_PATCH_OUTPUT_SCHEMA,
|
|
954
|
+
),
|
|
955
|
+
"artifact": RuntimeToolSpec(
|
|
956
|
+
name="artifact",
|
|
957
|
+
description=(
|
|
958
|
+
"Record a structured artifact such as a report, plan, "
|
|
959
|
+
"decision, data, or message."
|
|
960
|
+
),
|
|
961
|
+
handler=_artifact,
|
|
962
|
+
input_schema={
|
|
963
|
+
"type": "object",
|
|
964
|
+
"required": ["title", "kind", "content"],
|
|
965
|
+
"properties": {
|
|
966
|
+
"title": {
|
|
967
|
+
"type": "string",
|
|
968
|
+
"minLength": 1,
|
|
969
|
+
"maxLength": _SHORT_TEXT_MAX_LENGTH,
|
|
970
|
+
},
|
|
971
|
+
"kind": {
|
|
972
|
+
"type": "string",
|
|
973
|
+
"enum": list(_ARTIFACT_KINDS),
|
|
974
|
+
},
|
|
975
|
+
"content": {
|
|
976
|
+
"type": "string",
|
|
977
|
+
"minLength": 1,
|
|
978
|
+
"maxLength": _LONG_TEXT_MAX_LENGTH,
|
|
979
|
+
},
|
|
980
|
+
"format": {
|
|
981
|
+
"type": "string",
|
|
982
|
+
"enum": list(_ARTIFACT_FORMATS),
|
|
983
|
+
},
|
|
984
|
+
"tags": {
|
|
985
|
+
"type": "array",
|
|
986
|
+
"maxItems": _TAGS_MAX_ITEMS,
|
|
987
|
+
"items": {"type": "string"},
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
"additionalProperties": False,
|
|
991
|
+
},
|
|
992
|
+
output_schema=_ARTIFACT_OUTPUT_SCHEMA,
|
|
993
|
+
),
|
|
994
|
+
"decision_matrix": RuntimeToolSpec(
|
|
995
|
+
name="decision_matrix",
|
|
996
|
+
description="Rank options with weighted criteria for structured decisions.",
|
|
997
|
+
handler=_decision_matrix,
|
|
998
|
+
input_schema={
|
|
999
|
+
"type": "object",
|
|
1000
|
+
"required": ["question", "criteria", "options"],
|
|
1001
|
+
"properties": {
|
|
1002
|
+
"question": {"type": "string"},
|
|
1003
|
+
"criteria": {
|
|
1004
|
+
"type": "array",
|
|
1005
|
+
"minItems": 1,
|
|
1006
|
+
"maxItems": _DECISION_CRITERIA_MAX_ITEMS,
|
|
1007
|
+
"items": {
|
|
1008
|
+
"type": "object",
|
|
1009
|
+
"required": ["name", "weight"],
|
|
1010
|
+
"properties": {
|
|
1011
|
+
"name": {
|
|
1012
|
+
"type": "string",
|
|
1013
|
+
"minLength": 1,
|
|
1014
|
+
"maxLength": _SHORT_TEXT_MAX_LENGTH,
|
|
1015
|
+
},
|
|
1016
|
+
"weight": {"type": "number", "minimum": 0},
|
|
1017
|
+
},
|
|
1018
|
+
"additionalProperties": False,
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
"options": {
|
|
1022
|
+
"type": "array",
|
|
1023
|
+
"minItems": 2,
|
|
1024
|
+
"maxItems": _DECISION_OPTIONS_MAX_ITEMS,
|
|
1025
|
+
"items": {
|
|
1026
|
+
"type": "object",
|
|
1027
|
+
"required": ["name", "scores"],
|
|
1028
|
+
"properties": {
|
|
1029
|
+
"name": {
|
|
1030
|
+
"type": "string",
|
|
1031
|
+
"minLength": 1,
|
|
1032
|
+
"maxLength": _SHORT_TEXT_MAX_LENGTH,
|
|
1033
|
+
},
|
|
1034
|
+
"scores": {
|
|
1035
|
+
"type": "array",
|
|
1036
|
+
"minItems": 1,
|
|
1037
|
+
"items": {"type": "number"},
|
|
1038
|
+
},
|
|
1039
|
+
"rationale": {"type": "string"},
|
|
1040
|
+
},
|
|
1041
|
+
"additionalProperties": False,
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
"additionalProperties": False,
|
|
1046
|
+
},
|
|
1047
|
+
output_schema=_DECISION_MATRIX_OUTPUT_SCHEMA,
|
|
1048
|
+
),
|
|
1049
|
+
"note": RuntimeToolSpec(
|
|
1050
|
+
name="note",
|
|
1051
|
+
description="Record a short note as an artifact observation.",
|
|
1052
|
+
handler=_note,
|
|
1053
|
+
input_schema={
|
|
1054
|
+
"type": "object",
|
|
1055
|
+
"required": ["text"],
|
|
1056
|
+
"properties": {
|
|
1057
|
+
"text": {"type": "string", "maxLength": _LONG_TEXT_MAX_LENGTH}
|
|
1058
|
+
},
|
|
1059
|
+
"additionalProperties": False,
|
|
1060
|
+
},
|
|
1061
|
+
output_schema=_TEXT_OUTPUT_SCHEMA,
|
|
1062
|
+
),
|
|
1063
|
+
"http_request": RuntimeToolSpec(
|
|
1064
|
+
name="http_request",
|
|
1065
|
+
description=(
|
|
1066
|
+
"Fetch a URL with an HTTP GET request. This does not open a "
|
|
1067
|
+
"browser window; use open_url when the user asks to open a "
|
|
1068
|
+
"web page. This tool is policy-gated and should only run after "
|
|
1069
|
+
"explicit approval. Private, loopback, and link-local targets "
|
|
1070
|
+
"are rejected."
|
|
1071
|
+
),
|
|
1072
|
+
handler=_http_request,
|
|
1073
|
+
input_schema={
|
|
1074
|
+
"type": "object",
|
|
1075
|
+
"required": ["url"],
|
|
1076
|
+
"properties": {
|
|
1077
|
+
"url": {
|
|
1078
|
+
"type": "string",
|
|
1079
|
+
"minLength": 1,
|
|
1080
|
+
"maxLength": 2048,
|
|
1081
|
+
},
|
|
1082
|
+
"max_bytes": {
|
|
1083
|
+
"type": "number",
|
|
1084
|
+
"minimum": 1,
|
|
1085
|
+
"maximum": _HTTP_REQUEST_MAX_BYTES,
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
"additionalProperties": False,
|
|
1089
|
+
},
|
|
1090
|
+
output_schema=_HTTP_REQUEST_OUTPUT_SCHEMA,
|
|
1091
|
+
),
|
|
1092
|
+
"list_files": RuntimeToolSpec(
|
|
1093
|
+
name="list_files",
|
|
1094
|
+
description=(
|
|
1095
|
+
"List files and directories inside the current workspace with "
|
|
1096
|
+
"bounded depth and entry count."
|
|
1097
|
+
),
|
|
1098
|
+
handler=_list_files,
|
|
1099
|
+
input_schema={
|
|
1100
|
+
"type": "object",
|
|
1101
|
+
"properties": {
|
|
1102
|
+
"path": {"type": "string", "maxLength": 2048},
|
|
1103
|
+
"max_depth": {
|
|
1104
|
+
"type": "number",
|
|
1105
|
+
"minimum": 0,
|
|
1106
|
+
"maximum": _LIST_FILES_MAX_DEPTH,
|
|
1107
|
+
},
|
|
1108
|
+
"limit": {
|
|
1109
|
+
"type": "number",
|
|
1110
|
+
"minimum": 1,
|
|
1111
|
+
"maximum": _LIST_FILES_MAX_ENTRIES,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
"additionalProperties": False,
|
|
1115
|
+
},
|
|
1116
|
+
output_schema=_LIST_FILES_OUTPUT_SCHEMA,
|
|
1117
|
+
),
|
|
1118
|
+
"open_url": RuntimeToolSpec(
|
|
1119
|
+
name="open_url",
|
|
1120
|
+
description=(
|
|
1121
|
+
"Open an http:// or https:// URL in a local browser window on "
|
|
1122
|
+
"macOS. Uses Google Chrome automation first, with macOS open "
|
|
1123
|
+
"fallbacks. Use this when the user asks to open a web page."
|
|
1124
|
+
),
|
|
1125
|
+
handler=_open_url,
|
|
1126
|
+
input_schema={
|
|
1127
|
+
"type": "object",
|
|
1128
|
+
"required": ["url"],
|
|
1129
|
+
"properties": {
|
|
1130
|
+
"url": {
|
|
1131
|
+
"type": "string",
|
|
1132
|
+
"minLength": 1,
|
|
1133
|
+
"maxLength": 2048,
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
"additionalProperties": False,
|
|
1137
|
+
},
|
|
1138
|
+
output_schema=_OPEN_URL_OUTPUT_SCHEMA,
|
|
1139
|
+
),
|
|
1140
|
+
"open_app": RuntimeToolSpec(
|
|
1141
|
+
name="open_app",
|
|
1142
|
+
description=(
|
|
1143
|
+
"Open a local macOS application by application name. Use this "
|
|
1144
|
+
"when the user asks to open an installed app such as Chrome, "
|
|
1145
|
+
"Cursor, WeChat, Feishu, or Terminal. The input is an app name, "
|
|
1146
|
+
"not a path or shell command."
|
|
1147
|
+
),
|
|
1148
|
+
handler=_open_app,
|
|
1149
|
+
input_schema={
|
|
1150
|
+
"type": "object",
|
|
1151
|
+
"required": ["application"],
|
|
1152
|
+
"properties": {
|
|
1153
|
+
"application": {
|
|
1154
|
+
"type": "string",
|
|
1155
|
+
"minLength": 1,
|
|
1156
|
+
"maxLength": _APP_NAME_MAX_LENGTH,
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
"additionalProperties": False,
|
|
1160
|
+
},
|
|
1161
|
+
output_schema=_OPEN_APP_OUTPUT_SCHEMA,
|
|
1162
|
+
),
|
|
1163
|
+
"read_file": RuntimeToolSpec(
|
|
1164
|
+
name="read_file",
|
|
1165
|
+
description=(
|
|
1166
|
+
"Read a UTF-8 text file inside the current workspace with a "
|
|
1167
|
+
"bounded byte limit."
|
|
1168
|
+
),
|
|
1169
|
+
handler=_read_file,
|
|
1170
|
+
input_schema={
|
|
1171
|
+
"type": "object",
|
|
1172
|
+
"required": ["path"],
|
|
1173
|
+
"properties": {
|
|
1174
|
+
"path": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
1175
|
+
"max_bytes": {
|
|
1176
|
+
"type": "number",
|
|
1177
|
+
"minimum": 1,
|
|
1178
|
+
"maximum": _READ_FILE_MAX_BYTES,
|
|
1179
|
+
},
|
|
1180
|
+
},
|
|
1181
|
+
"additionalProperties": False,
|
|
1182
|
+
},
|
|
1183
|
+
output_schema=_READ_FILE_OUTPUT_SCHEMA,
|
|
1184
|
+
),
|
|
1185
|
+
"workspace_write": RuntimeToolSpec(
|
|
1186
|
+
name="workspace_write",
|
|
1187
|
+
description=(
|
|
1188
|
+
"Write UTF-8 text into the runtime virtual workspace. "
|
|
1189
|
+
"Kinds are workspace, reports, logs, policies, and memories. "
|
|
1190
|
+
"Paths are relative to the selected virtual directory and "
|
|
1191
|
+
"cannot escape it."
|
|
1192
|
+
),
|
|
1193
|
+
handler=lambda payload: _workspace_write(
|
|
1194
|
+
payload,
|
|
1195
|
+
runtime_workspace_dir=runtime_workspace_dir,
|
|
1196
|
+
),
|
|
1197
|
+
input_schema={
|
|
1198
|
+
"type": "object",
|
|
1199
|
+
"required": ["kind", "path", "content"],
|
|
1200
|
+
"properties": {
|
|
1201
|
+
"kind": {
|
|
1202
|
+
"type": "string",
|
|
1203
|
+
"enum": list(VIRTUAL_WORKSPACE_KINDS),
|
|
1204
|
+
},
|
|
1205
|
+
"path": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
1206
|
+
"content": {
|
|
1207
|
+
"type": "string",
|
|
1208
|
+
"minLength": 1,
|
|
1209
|
+
"maxLength": _LONG_TEXT_MAX_LENGTH,
|
|
1210
|
+
},
|
|
1211
|
+
"metadata": {"type": "object"},
|
|
1212
|
+
},
|
|
1213
|
+
"additionalProperties": False,
|
|
1214
|
+
},
|
|
1215
|
+
output_schema=_WORKSPACE_ASSET_METADATA_SCHEMA,
|
|
1216
|
+
),
|
|
1217
|
+
"workspace_read": RuntimeToolSpec(
|
|
1218
|
+
name="workspace_read",
|
|
1219
|
+
description="Read a UTF-8 text asset from the runtime virtual workspace.",
|
|
1220
|
+
handler=lambda payload: _workspace_read(
|
|
1221
|
+
payload,
|
|
1222
|
+
runtime_workspace_dir=runtime_workspace_dir,
|
|
1223
|
+
),
|
|
1224
|
+
input_schema={
|
|
1225
|
+
"type": "object",
|
|
1226
|
+
"required": ["kind", "path"],
|
|
1227
|
+
"properties": {
|
|
1228
|
+
"kind": {
|
|
1229
|
+
"type": "string",
|
|
1230
|
+
"enum": list(VIRTUAL_WORKSPACE_KINDS),
|
|
1231
|
+
},
|
|
1232
|
+
"path": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
1233
|
+
"max_bytes": {
|
|
1234
|
+
"type": "number",
|
|
1235
|
+
"minimum": 1,
|
|
1236
|
+
"maximum": _READ_FILE_MAX_BYTES,
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
"additionalProperties": False,
|
|
1240
|
+
},
|
|
1241
|
+
output_schema=_WORKSPACE_READ_OUTPUT_SCHEMA,
|
|
1242
|
+
),
|
|
1243
|
+
"workspace_history": RuntimeToolSpec(
|
|
1244
|
+
name="workspace_history",
|
|
1245
|
+
description=(
|
|
1246
|
+
"Read previous versions saved before overwriting a UTF-8 text "
|
|
1247
|
+
"asset in the runtime virtual workspace."
|
|
1248
|
+
),
|
|
1249
|
+
handler=lambda payload: _workspace_history(
|
|
1250
|
+
payload,
|
|
1251
|
+
runtime_workspace_dir=runtime_workspace_dir,
|
|
1252
|
+
),
|
|
1253
|
+
input_schema={
|
|
1254
|
+
"type": "object",
|
|
1255
|
+
"required": ["kind", "path"],
|
|
1256
|
+
"properties": {
|
|
1257
|
+
"kind": {
|
|
1258
|
+
"type": "string",
|
|
1259
|
+
"enum": list(VIRTUAL_WORKSPACE_KINDS),
|
|
1260
|
+
},
|
|
1261
|
+
"path": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
1262
|
+
"limit": {"type": "number", "minimum": 1, "maximum": 50},
|
|
1263
|
+
"max_bytes": {
|
|
1264
|
+
"type": "number",
|
|
1265
|
+
"minimum": 1,
|
|
1266
|
+
"maximum": _READ_FILE_MAX_BYTES,
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
"additionalProperties": False,
|
|
1270
|
+
},
|
|
1271
|
+
output_schema=_WORKSPACE_HISTORY_OUTPUT_SCHEMA,
|
|
1272
|
+
),
|
|
1273
|
+
"workspace_diff": RuntimeToolSpec(
|
|
1274
|
+
name="workspace_diff",
|
|
1275
|
+
description=(
|
|
1276
|
+
"Compare the current UTF-8 text asset in the runtime virtual "
|
|
1277
|
+
"workspace against a saved previous version and return a bounded "
|
|
1278
|
+
"unified diff."
|
|
1279
|
+
),
|
|
1280
|
+
handler=lambda payload: _workspace_diff(
|
|
1281
|
+
payload,
|
|
1282
|
+
runtime_workspace_dir=runtime_workspace_dir,
|
|
1283
|
+
),
|
|
1284
|
+
input_schema={
|
|
1285
|
+
"type": "object",
|
|
1286
|
+
"required": ["kind", "path"],
|
|
1287
|
+
"properties": {
|
|
1288
|
+
"kind": {
|
|
1289
|
+
"type": "string",
|
|
1290
|
+
"enum": list(VIRTUAL_WORKSPACE_KINDS),
|
|
1291
|
+
},
|
|
1292
|
+
"path": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
1293
|
+
"revision_id": {"type": "string", "maxLength": 120},
|
|
1294
|
+
"context_lines": {"type": "number", "minimum": 0, "maximum": 20},
|
|
1295
|
+
"max_bytes": {
|
|
1296
|
+
"type": "number",
|
|
1297
|
+
"minimum": 1,
|
|
1298
|
+
"maximum": _READ_FILE_MAX_BYTES,
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
"additionalProperties": False,
|
|
1302
|
+
},
|
|
1303
|
+
output_schema=_WORKSPACE_DIFF_OUTPUT_SCHEMA,
|
|
1304
|
+
),
|
|
1305
|
+
"workspace_restore": RuntimeToolSpec(
|
|
1306
|
+
name="workspace_restore",
|
|
1307
|
+
description=(
|
|
1308
|
+
"Restore a runtime virtual workspace text asset to a reviewed "
|
|
1309
|
+
"saved revision. Requires the expected current SHA-256 so a "
|
|
1310
|
+
"concurrent edit cannot be overwritten silently. The current "
|
|
1311
|
+
"content is saved as a new revision before restoration."
|
|
1312
|
+
),
|
|
1313
|
+
handler=lambda payload: _workspace_restore(
|
|
1314
|
+
payload,
|
|
1315
|
+
runtime_workspace_dir=runtime_workspace_dir,
|
|
1316
|
+
),
|
|
1317
|
+
input_schema={
|
|
1318
|
+
"type": "object",
|
|
1319
|
+
"required": [
|
|
1320
|
+
"kind",
|
|
1321
|
+
"path",
|
|
1322
|
+
"revision_id",
|
|
1323
|
+
"expected_current_sha256",
|
|
1324
|
+
"expected_revision_sha256",
|
|
1325
|
+
],
|
|
1326
|
+
"properties": {
|
|
1327
|
+
"kind": {
|
|
1328
|
+
"type": "string",
|
|
1329
|
+
"enum": list(VIRTUAL_WORKSPACE_KINDS),
|
|
1330
|
+
},
|
|
1331
|
+
"path": {"type": "string", "minLength": 1, "maxLength": 2048},
|
|
1332
|
+
"revision_id": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1333
|
+
"expected_current_sha256": {
|
|
1334
|
+
"type": "string",
|
|
1335
|
+
"minLength": 64,
|
|
1336
|
+
"maxLength": 64,
|
|
1337
|
+
},
|
|
1338
|
+
"expected_revision_sha256": {
|
|
1339
|
+
"type": "string",
|
|
1340
|
+
"minLength": 64,
|
|
1341
|
+
"maxLength": 64,
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
"additionalProperties": False,
|
|
1345
|
+
},
|
|
1346
|
+
output_schema=_WORKSPACE_RESTORE_OUTPUT_SCHEMA,
|
|
1347
|
+
),
|
|
1348
|
+
"workspace_list": RuntimeToolSpec(
|
|
1349
|
+
name="workspace_list",
|
|
1350
|
+
description=(
|
|
1351
|
+
"List assets inside a runtime virtual workspace directory with "
|
|
1352
|
+
"bounded depth and entry count."
|
|
1353
|
+
),
|
|
1354
|
+
handler=lambda payload: _workspace_list(
|
|
1355
|
+
payload,
|
|
1356
|
+
runtime_workspace_dir=runtime_workspace_dir,
|
|
1357
|
+
),
|
|
1358
|
+
input_schema={
|
|
1359
|
+
"type": "object",
|
|
1360
|
+
"required": ["kind"],
|
|
1361
|
+
"properties": {
|
|
1362
|
+
"kind": {
|
|
1363
|
+
"type": "string",
|
|
1364
|
+
"enum": list(VIRTUAL_WORKSPACE_KINDS),
|
|
1365
|
+
},
|
|
1366
|
+
"path": {"type": "string", "maxLength": 2048},
|
|
1367
|
+
"max_depth": {
|
|
1368
|
+
"type": "number",
|
|
1369
|
+
"minimum": 0,
|
|
1370
|
+
"maximum": _LIST_FILES_MAX_DEPTH,
|
|
1371
|
+
},
|
|
1372
|
+
"limit": {
|
|
1373
|
+
"type": "number",
|
|
1374
|
+
"minimum": 1,
|
|
1375
|
+
"maximum": _LIST_FILES_MAX_ENTRIES,
|
|
1376
|
+
},
|
|
1377
|
+
},
|
|
1378
|
+
"additionalProperties": False,
|
|
1379
|
+
},
|
|
1380
|
+
output_schema=_WORKSPACE_LIST_OUTPUT_SCHEMA,
|
|
1381
|
+
),
|
|
1382
|
+
"workspace_search": RuntimeToolSpec(
|
|
1383
|
+
name="workspace_search",
|
|
1384
|
+
description=(
|
|
1385
|
+
"Search UTF-8 text assets inside a runtime virtual workspace "
|
|
1386
|
+
"directory with bounded depth, byte, and result limits."
|
|
1387
|
+
),
|
|
1388
|
+
handler=lambda payload: _workspace_search(
|
|
1389
|
+
payload,
|
|
1390
|
+
runtime_workspace_dir=runtime_workspace_dir,
|
|
1391
|
+
),
|
|
1392
|
+
input_schema={
|
|
1393
|
+
"type": "object",
|
|
1394
|
+
"required": ["kind", "query"],
|
|
1395
|
+
"properties": {
|
|
1396
|
+
"kind": {
|
|
1397
|
+
"type": "string",
|
|
1398
|
+
"enum": list(VIRTUAL_WORKSPACE_KINDS),
|
|
1399
|
+
},
|
|
1400
|
+
"query": {"type": "string", "minLength": 1, "maxLength": 500},
|
|
1401
|
+
"path": {"type": "string", "maxLength": 2048},
|
|
1402
|
+
"max_depth": {
|
|
1403
|
+
"type": "number",
|
|
1404
|
+
"minimum": 0,
|
|
1405
|
+
"maximum": _LIST_FILES_MAX_DEPTH,
|
|
1406
|
+
},
|
|
1407
|
+
"limit": {"type": "number", "minimum": 1, "maximum": 50},
|
|
1408
|
+
"max_bytes": {
|
|
1409
|
+
"type": "number",
|
|
1410
|
+
"minimum": 1,
|
|
1411
|
+
"maximum": _READ_FILE_MAX_BYTES,
|
|
1412
|
+
},
|
|
1413
|
+
},
|
|
1414
|
+
"additionalProperties": False,
|
|
1415
|
+
},
|
|
1416
|
+
output_schema=_WORKSPACE_SEARCH_OUTPUT_SCHEMA,
|
|
1417
|
+
),
|
|
1418
|
+
"memory_put": RuntimeToolSpec(
|
|
1419
|
+
name="memory_put",
|
|
1420
|
+
description=(
|
|
1421
|
+
"Store short-term structured runtime memory in Redis by namespace "
|
|
1422
|
+
"and key. Requires KAGENT_REDIS_URL or service redis_url."
|
|
1423
|
+
),
|
|
1424
|
+
handler=lambda payload: _memory_put(
|
|
1425
|
+
payload,
|
|
1426
|
+
redis_url=active_redis_url,
|
|
1427
|
+
timeout_seconds=active_backend_timeout,
|
|
1428
|
+
),
|
|
1429
|
+
input_schema={
|
|
1430
|
+
"type": "object",
|
|
1431
|
+
"required": ["namespace", "key", "value"],
|
|
1432
|
+
"properties": {
|
|
1433
|
+
"namespace": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1434
|
+
"key": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1435
|
+
"value": {},
|
|
1436
|
+
"ttl_seconds": {"type": "number", "minimum": 0},
|
|
1437
|
+
},
|
|
1438
|
+
"additionalProperties": False,
|
|
1439
|
+
},
|
|
1440
|
+
output_schema=_MEMORY_PUT_OUTPUT_SCHEMA,
|
|
1441
|
+
),
|
|
1442
|
+
"memory_get": RuntimeToolSpec(
|
|
1443
|
+
name="memory_get",
|
|
1444
|
+
description=(
|
|
1445
|
+
"Read short-term structured runtime memory from Redis by "
|
|
1446
|
+
"namespace and key."
|
|
1447
|
+
),
|
|
1448
|
+
handler=lambda payload: _memory_get(
|
|
1449
|
+
payload,
|
|
1450
|
+
redis_url=active_redis_url,
|
|
1451
|
+
timeout_seconds=active_backend_timeout,
|
|
1452
|
+
),
|
|
1453
|
+
input_schema={
|
|
1454
|
+
"type": "object",
|
|
1455
|
+
"required": ["namespace", "key"],
|
|
1456
|
+
"properties": {
|
|
1457
|
+
"namespace": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1458
|
+
"key": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1459
|
+
},
|
|
1460
|
+
"additionalProperties": False,
|
|
1461
|
+
},
|
|
1462
|
+
output_schema=_MEMORY_GET_OUTPUT_SCHEMA,
|
|
1463
|
+
),
|
|
1464
|
+
"memory_upsert": RuntimeToolSpec(
|
|
1465
|
+
name="memory_upsert",
|
|
1466
|
+
description=(
|
|
1467
|
+
"Store long-term semantic memory in Milvus. The caller must "
|
|
1468
|
+
"provide an embedding vector; this runtime does not invent "
|
|
1469
|
+
"vectors from text."
|
|
1470
|
+
),
|
|
1471
|
+
handler=lambda payload: _memory_upsert(
|
|
1472
|
+
payload,
|
|
1473
|
+
milvus_url=active_milvus_url,
|
|
1474
|
+
timeout_seconds=active_backend_timeout,
|
|
1475
|
+
),
|
|
1476
|
+
input_schema={
|
|
1477
|
+
"type": "object",
|
|
1478
|
+
"required": ["collection", "memory_id", "text", "vector"],
|
|
1479
|
+
"properties": {
|
|
1480
|
+
"collection": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1481
|
+
"memory_id": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1482
|
+
"text": {"type": "string", "minLength": 1, "maxLength": 20000},
|
|
1483
|
+
"vector": {
|
|
1484
|
+
"type": "array",
|
|
1485
|
+
"minItems": 1,
|
|
1486
|
+
"maxItems": 4096,
|
|
1487
|
+
"items": {"type": "number"},
|
|
1488
|
+
},
|
|
1489
|
+
"metadata": {"type": "object"},
|
|
1490
|
+
},
|
|
1491
|
+
"additionalProperties": False,
|
|
1492
|
+
},
|
|
1493
|
+
output_schema=_MEMORY_UPSERT_OUTPUT_SCHEMA,
|
|
1494
|
+
),
|
|
1495
|
+
"memory_search": RuntimeToolSpec(
|
|
1496
|
+
name="memory_search",
|
|
1497
|
+
description=(
|
|
1498
|
+
"Search long-term semantic memory in Milvus with an embedding "
|
|
1499
|
+
"vector. Requires KAGENT_MILVUS_URL or service milvus_url."
|
|
1500
|
+
),
|
|
1501
|
+
handler=lambda payload: _memory_search(
|
|
1502
|
+
payload,
|
|
1503
|
+
milvus_url=active_milvus_url,
|
|
1504
|
+
timeout_seconds=active_backend_timeout,
|
|
1505
|
+
),
|
|
1506
|
+
input_schema={
|
|
1507
|
+
"type": "object",
|
|
1508
|
+
"required": ["collection", "vector"],
|
|
1509
|
+
"properties": {
|
|
1510
|
+
"collection": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1511
|
+
"vector": {
|
|
1512
|
+
"type": "array",
|
|
1513
|
+
"minItems": 1,
|
|
1514
|
+
"maxItems": 4096,
|
|
1515
|
+
"items": {"type": "number"},
|
|
1516
|
+
},
|
|
1517
|
+
"limit": {"type": "number", "minimum": 1, "maximum": 50},
|
|
1518
|
+
},
|
|
1519
|
+
"additionalProperties": False,
|
|
1520
|
+
},
|
|
1521
|
+
output_schema=_MEMORY_SEARCH_OUTPUT_SCHEMA,
|
|
1522
|
+
),
|
|
1523
|
+
"memory_remember": RuntimeToolSpec(
|
|
1524
|
+
name="memory_remember",
|
|
1525
|
+
description=(
|
|
1526
|
+
"Embed text with the configured OpenAI-compatible embedding "
|
|
1527
|
+
"provider and store it in Milvus long-term memory."
|
|
1528
|
+
),
|
|
1529
|
+
handler=lambda payload: _memory_remember(
|
|
1530
|
+
payload,
|
|
1531
|
+
milvus_url=active_milvus_url,
|
|
1532
|
+
embedding_config=active_embedding_config,
|
|
1533
|
+
timeout_seconds=active_backend_timeout,
|
|
1534
|
+
),
|
|
1535
|
+
input_schema={
|
|
1536
|
+
"type": "object",
|
|
1537
|
+
"required": ["collection", "memory_id", "text"],
|
|
1538
|
+
"properties": {
|
|
1539
|
+
"collection": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1540
|
+
"memory_id": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1541
|
+
"text": {"type": "string", "minLength": 1, "maxLength": 20000},
|
|
1542
|
+
"metadata": {"type": "object"},
|
|
1543
|
+
},
|
|
1544
|
+
"additionalProperties": False,
|
|
1545
|
+
},
|
|
1546
|
+
output_schema=_MEMORY_REMEMBER_OUTPUT_SCHEMA,
|
|
1547
|
+
),
|
|
1548
|
+
"memory_recall": RuntimeToolSpec(
|
|
1549
|
+
name="memory_recall",
|
|
1550
|
+
description=(
|
|
1551
|
+
"Embed a text query with the configured OpenAI-compatible "
|
|
1552
|
+
"embedding provider and search Milvus long-term memory."
|
|
1553
|
+
),
|
|
1554
|
+
handler=lambda payload: _memory_recall(
|
|
1555
|
+
payload,
|
|
1556
|
+
milvus_url=active_milvus_url,
|
|
1557
|
+
embedding_config=active_embedding_config,
|
|
1558
|
+
timeout_seconds=active_backend_timeout,
|
|
1559
|
+
),
|
|
1560
|
+
input_schema={
|
|
1561
|
+
"type": "object",
|
|
1562
|
+
"required": ["collection", "query"],
|
|
1563
|
+
"properties": {
|
|
1564
|
+
"collection": {"type": "string", "minLength": 1, "maxLength": 120},
|
|
1565
|
+
"query": {"type": "string", "minLength": 1, "maxLength": 20000},
|
|
1566
|
+
"limit": {"type": "number", "minimum": 1, "maximum": 50},
|
|
1567
|
+
},
|
|
1568
|
+
"additionalProperties": False,
|
|
1569
|
+
},
|
|
1570
|
+
output_schema=_MEMORY_RECALL_OUTPUT_SCHEMA,
|
|
1571
|
+
),
|
|
1572
|
+
"shell_command": RuntimeToolSpec(
|
|
1573
|
+
name="shell_command",
|
|
1574
|
+
description=(
|
|
1575
|
+
"Run a bounded non-interactive shell command inside the current "
|
|
1576
|
+
"workspace. Captures stdout/stderr, returns the process exit code, "
|
|
1577
|
+
"rejects workspace escapes and obvious interactive/background "
|
|
1578
|
+
"commands, and is policy-gated by default."
|
|
1579
|
+
),
|
|
1580
|
+
handler=_shell_command,
|
|
1581
|
+
input_schema={
|
|
1582
|
+
"type": "object",
|
|
1583
|
+
"required": ["command"],
|
|
1584
|
+
"properties": {
|
|
1585
|
+
"command": {
|
|
1586
|
+
"type": "string",
|
|
1587
|
+
"minLength": 1,
|
|
1588
|
+
"maxLength": _SHELL_COMMAND_MAX_LENGTH,
|
|
1589
|
+
},
|
|
1590
|
+
"cwd": {"type": "string", "maxLength": 2048},
|
|
1591
|
+
"timeout_seconds": {
|
|
1592
|
+
"type": "number",
|
|
1593
|
+
"minimum": 1,
|
|
1594
|
+
"maximum": _SHELL_COMMAND_MAX_TIMEOUT_SECONDS,
|
|
1595
|
+
},
|
|
1596
|
+
"max_output_bytes": {
|
|
1597
|
+
"type": "number",
|
|
1598
|
+
"minimum": 1,
|
|
1599
|
+
"maximum": _SHELL_COMMAND_MAX_OUTPUT_BYTES,
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
"additionalProperties": False,
|
|
1603
|
+
},
|
|
1604
|
+
output_schema=_SHELL_COMMAND_OUTPUT_SCHEMA,
|
|
1605
|
+
timeout_seconds=_SHELL_COMMAND_MAX_TIMEOUT_SECONDS + 1,
|
|
1606
|
+
),
|
|
1607
|
+
"skill_list": RuntimeToolSpec(
|
|
1608
|
+
name="skill_list",
|
|
1609
|
+
description="List installed runtime skills available to the agent.",
|
|
1610
|
+
handler=lambda payload: _skill_list(
|
|
1611
|
+
payload,
|
|
1612
|
+
runtime_skills_dir=runtime_skills_dir,
|
|
1613
|
+
),
|
|
1614
|
+
input_schema={
|
|
1615
|
+
"type": "object",
|
|
1616
|
+
"properties": {},
|
|
1617
|
+
"additionalProperties": False,
|
|
1618
|
+
},
|
|
1619
|
+
output_schema=_SKILL_LIST_OUTPUT_SCHEMA,
|
|
1620
|
+
),
|
|
1621
|
+
"skill_get": RuntimeToolSpec(
|
|
1622
|
+
name="skill_get",
|
|
1623
|
+
description="Read one installed runtime skill's instructions by name.",
|
|
1624
|
+
handler=lambda payload: _skill_get(
|
|
1625
|
+
payload,
|
|
1626
|
+
runtime_skills_dir=runtime_skills_dir,
|
|
1627
|
+
),
|
|
1628
|
+
input_schema={
|
|
1629
|
+
"type": "object",
|
|
1630
|
+
"required": ["name"],
|
|
1631
|
+
"properties": {
|
|
1632
|
+
"name": {"type": "string", "minLength": 1, "maxLength": 80},
|
|
1633
|
+
},
|
|
1634
|
+
"additionalProperties": False,
|
|
1635
|
+
},
|
|
1636
|
+
output_schema=_SKILL_GET_OUTPUT_SCHEMA,
|
|
1637
|
+
),
|
|
1638
|
+
"task_list": RuntimeToolSpec(
|
|
1639
|
+
name="task_list",
|
|
1640
|
+
description="Create a structured task list with normalized status counts.",
|
|
1641
|
+
handler=_task_list,
|
|
1642
|
+
input_schema={
|
|
1643
|
+
"type": "object",
|
|
1644
|
+
"required": ["items"],
|
|
1645
|
+
"properties": {
|
|
1646
|
+
"items": {
|
|
1647
|
+
"type": "array",
|
|
1648
|
+
"minItems": 1,
|
|
1649
|
+
"maxItems": _TASK_ITEMS_MAX_ITEMS,
|
|
1650
|
+
"items": {
|
|
1651
|
+
"type": "object",
|
|
1652
|
+
"required": ["title"],
|
|
1653
|
+
"properties": {
|
|
1654
|
+
"title": {
|
|
1655
|
+
"type": "string",
|
|
1656
|
+
"minLength": 1,
|
|
1657
|
+
"maxLength": _TASK_TITLE_MAX_LENGTH,
|
|
1658
|
+
},
|
|
1659
|
+
"status": {
|
|
1660
|
+
"type": "string",
|
|
1661
|
+
"enum": list(_TASK_STATUSES),
|
|
1662
|
+
},
|
|
1663
|
+
"priority": {
|
|
1664
|
+
"type": "string",
|
|
1665
|
+
"enum": list(_TASK_PRIORITIES),
|
|
1666
|
+
},
|
|
1667
|
+
"owner": {"type": "string"},
|
|
1668
|
+
"due": {"type": "string"},
|
|
1669
|
+
},
|
|
1670
|
+
"additionalProperties": False,
|
|
1671
|
+
},
|
|
1672
|
+
},
|
|
1673
|
+
},
|
|
1674
|
+
"additionalProperties": False,
|
|
1675
|
+
},
|
|
1676
|
+
output_schema=_TASK_LIST_OUTPUT_SCHEMA,
|
|
1677
|
+
),
|
|
1678
|
+
"task_transition": RuntimeToolSpec(
|
|
1679
|
+
name="task_transition",
|
|
1680
|
+
description=(
|
|
1681
|
+
"Advance one task through the runtime task lifecycle using a "
|
|
1682
|
+
"validated state-machine transition."
|
|
1683
|
+
),
|
|
1684
|
+
handler=_task_transition,
|
|
1685
|
+
input_schema={
|
|
1686
|
+
"type": "object",
|
|
1687
|
+
"required": ["state", "event"],
|
|
1688
|
+
"properties": {
|
|
1689
|
+
"state": {"type": "string", "enum": list(TASK_STATES)},
|
|
1690
|
+
"event": {"type": "string", "enum": list(TASK_EVENTS)},
|
|
1691
|
+
},
|
|
1692
|
+
"additionalProperties": False,
|
|
1693
|
+
},
|
|
1694
|
+
output_schema=_TASK_TRANSITION_OUTPUT_SCHEMA,
|
|
1695
|
+
),
|
|
1696
|
+
"rubric_score": RuntimeToolSpec(
|
|
1697
|
+
name="rubric_score",
|
|
1698
|
+
description=(
|
|
1699
|
+
"Score a result against pass/fail rubric criteria and report "
|
|
1700
|
+
"blocking failures."
|
|
1701
|
+
),
|
|
1702
|
+
handler=_rubric_score,
|
|
1703
|
+
input_schema={
|
|
1704
|
+
"type": "object",
|
|
1705
|
+
"required": ["criteria"],
|
|
1706
|
+
"properties": {
|
|
1707
|
+
"criteria": {
|
|
1708
|
+
"type": "array",
|
|
1709
|
+
"minItems": 1,
|
|
1710
|
+
"maxItems": _RUBRIC_CRITERIA_MAX_ITEMS,
|
|
1711
|
+
"items": {
|
|
1712
|
+
"type": "object",
|
|
1713
|
+
"required": ["name", "passed"],
|
|
1714
|
+
"properties": {
|
|
1715
|
+
"name": {
|
|
1716
|
+
"type": "string",
|
|
1717
|
+
"minLength": 1,
|
|
1718
|
+
"maxLength": _SHORT_TEXT_MAX_LENGTH,
|
|
1719
|
+
},
|
|
1720
|
+
"passed": {"type": "boolean"},
|
|
1721
|
+
"severity": {
|
|
1722
|
+
"type": "string",
|
|
1723
|
+
"enum": list(_RUBRIC_SEVERITIES),
|
|
1724
|
+
},
|
|
1725
|
+
"evidence": {
|
|
1726
|
+
"type": "string",
|
|
1727
|
+
"maxLength": _LONG_TEXT_MAX_LENGTH,
|
|
1728
|
+
},
|
|
1729
|
+
},
|
|
1730
|
+
"additionalProperties": False,
|
|
1731
|
+
},
|
|
1732
|
+
},
|
|
1733
|
+
},
|
|
1734
|
+
"additionalProperties": False,
|
|
1735
|
+
},
|
|
1736
|
+
output_schema=_RUBRIC_SCORE_OUTPUT_SCHEMA,
|
|
1737
|
+
),
|
|
1738
|
+
"transform_text": RuntimeToolSpec(
|
|
1739
|
+
name="transform_text",
|
|
1740
|
+
description="Transform text with uppercase, lowercase, reverse, or trim modes.",
|
|
1741
|
+
handler=_transform_text,
|
|
1742
|
+
input_schema={
|
|
1743
|
+
"type": "object",
|
|
1744
|
+
"required": ["text", "mode"],
|
|
1745
|
+
"properties": {
|
|
1746
|
+
"text": {"type": "string"},
|
|
1747
|
+
"mode": {
|
|
1748
|
+
"type": "string",
|
|
1749
|
+
"enum": ["uppercase", "lowercase", "reverse", "trim"],
|
|
1750
|
+
},
|
|
1751
|
+
},
|
|
1752
|
+
"additionalProperties": False,
|
|
1753
|
+
},
|
|
1754
|
+
output_schema=_TEXT_OUTPUT_SCHEMA,
|
|
1755
|
+
),
|
|
1756
|
+
}
|
|
1757
|
+
if include_delegate_tool:
|
|
1758
|
+
tools["delegate_task"] = RuntimeToolSpec(
|
|
1759
|
+
name="delegate_task",
|
|
1760
|
+
description=(
|
|
1761
|
+
"Delegate a bounded subtask to a child kagent runtime. Child "
|
|
1762
|
+
"runs return a compact summary and cannot delegate again by default."
|
|
1763
|
+
),
|
|
1764
|
+
handler=lambda payload: _delegate_task(
|
|
1765
|
+
payload,
|
|
1766
|
+
delegate_runner=delegate_runner,
|
|
1767
|
+
),
|
|
1768
|
+
input_schema={
|
|
1769
|
+
"type": "object",
|
|
1770
|
+
"required": ["goal"],
|
|
1771
|
+
"properties": {
|
|
1772
|
+
"goal": {
|
|
1773
|
+
"type": "string",
|
|
1774
|
+
"minLength": 1,
|
|
1775
|
+
"maxLength": _DELEGATE_GOAL_MAX_LENGTH,
|
|
1776
|
+
},
|
|
1777
|
+
"max_iterations": {
|
|
1778
|
+
"type": "number",
|
|
1779
|
+
"minimum": 1,
|
|
1780
|
+
"maximum": _DELEGATE_MAX_ITERATIONS,
|
|
1781
|
+
},
|
|
1782
|
+
},
|
|
1783
|
+
"additionalProperties": False,
|
|
1784
|
+
},
|
|
1785
|
+
output_schema=_DELEGATE_TASK_OUTPUT_SCHEMA,
|
|
1786
|
+
timeout_seconds=30.0,
|
|
1787
|
+
)
|
|
1788
|
+
return tools
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
def registered_runtime_tool_metadata() -> list[Dict[str, Any]]:
|
|
1792
|
+
return runtime_tool_metadata(default_runtime_tools())
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
def runtime_tool_metadata(tools: Dict[str, RuntimeToolSpec]) -> list[Dict[str, Any]]:
|
|
1796
|
+
default_allowed_tools = RuntimePolicy().allowed_tools
|
|
1797
|
+
return [
|
|
1798
|
+
{
|
|
1799
|
+
"name": tool.name,
|
|
1800
|
+
"description": tool.description,
|
|
1801
|
+
"approval_required_by_default": str(
|
|
1802
|
+
tool.name not in default_allowed_tools
|
|
1803
|
+
).lower(),
|
|
1804
|
+
"input_schema": tool.input_schema,
|
|
1805
|
+
"output_schema": tool.output_schema,
|
|
1806
|
+
"timeout_seconds": f"{tool.timeout_seconds:.1f}",
|
|
1807
|
+
}
|
|
1808
|
+
for tool in sorted(tools.values(), key=lambda item: item.name)
|
|
1809
|
+
]
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
def execute_runtime_tool(
|
|
1813
|
+
registry: Dict[str, RuntimeToolSpec],
|
|
1814
|
+
tool_name: str,
|
|
1815
|
+
input_payload: Dict[str, Any],
|
|
1816
|
+
*,
|
|
1817
|
+
action_id: str = "",
|
|
1818
|
+
) -> AgentObservation:
|
|
1819
|
+
started_at = _utc_timestamp()
|
|
1820
|
+
started_timer = time.perf_counter()
|
|
1821
|
+
tool = registry.get(tool_name)
|
|
1822
|
+
if tool is None:
|
|
1823
|
+
return AgentObservation(
|
|
1824
|
+
action_id=action_id,
|
|
1825
|
+
tool=tool_name,
|
|
1826
|
+
status="failed",
|
|
1827
|
+
output={},
|
|
1828
|
+
error_code="tool_not_found",
|
|
1829
|
+
error="tool is not registered",
|
|
1830
|
+
started_at=started_at,
|
|
1831
|
+
completed_at=_utc_timestamp(),
|
|
1832
|
+
duration_seconds=_duration_since(started_timer),
|
|
1833
|
+
)
|
|
1834
|
+
try:
|
|
1835
|
+
_validate_tool_input(input_payload, tool.input_schema)
|
|
1836
|
+
output = _run_tool_handler(tool, input_payload)
|
|
1837
|
+
except TimeoutError as exc:
|
|
1838
|
+
return AgentObservation(
|
|
1839
|
+
action_id=action_id,
|
|
1840
|
+
tool=tool_name,
|
|
1841
|
+
status="failed",
|
|
1842
|
+
output={},
|
|
1843
|
+
error_code="tool_execution_timeout",
|
|
1844
|
+
error=str(exc) or "tool execution exceeded timeout",
|
|
1845
|
+
started_at=started_at,
|
|
1846
|
+
completed_at=_utc_timestamp(),
|
|
1847
|
+
duration_seconds=_duration_since(started_timer),
|
|
1848
|
+
)
|
|
1849
|
+
except ValueError as exc:
|
|
1850
|
+
return AgentObservation(
|
|
1851
|
+
action_id=action_id,
|
|
1852
|
+
tool=tool_name,
|
|
1853
|
+
status="failed",
|
|
1854
|
+
output={},
|
|
1855
|
+
error_code="invalid_tool_input",
|
|
1856
|
+
error=str(exc),
|
|
1857
|
+
started_at=started_at,
|
|
1858
|
+
completed_at=_utc_timestamp(),
|
|
1859
|
+
duration_seconds=_duration_since(started_timer),
|
|
1860
|
+
)
|
|
1861
|
+
except Exception as exc:
|
|
1862
|
+
return AgentObservation(
|
|
1863
|
+
action_id=action_id,
|
|
1864
|
+
tool=tool_name,
|
|
1865
|
+
status="failed",
|
|
1866
|
+
output={},
|
|
1867
|
+
error_code="tool_execution_failed",
|
|
1868
|
+
error=str(exc) or type(exc).__name__,
|
|
1869
|
+
started_at=started_at,
|
|
1870
|
+
completed_at=_utc_timestamp(),
|
|
1871
|
+
duration_seconds=_duration_since(started_timer),
|
|
1872
|
+
)
|
|
1873
|
+
try:
|
|
1874
|
+
_validate_tool_input(output, tool.output_schema, "output")
|
|
1875
|
+
except ValueError as exc:
|
|
1876
|
+
return AgentObservation(
|
|
1877
|
+
action_id=action_id,
|
|
1878
|
+
tool=tool_name,
|
|
1879
|
+
status="failed",
|
|
1880
|
+
output={},
|
|
1881
|
+
error_code="invalid_tool_output",
|
|
1882
|
+
error=str(exc),
|
|
1883
|
+
started_at=started_at,
|
|
1884
|
+
completed_at=_utc_timestamp(),
|
|
1885
|
+
duration_seconds=_duration_since(started_timer),
|
|
1886
|
+
)
|
|
1887
|
+
return AgentObservation(
|
|
1888
|
+
action_id=action_id,
|
|
1889
|
+
tool=tool_name,
|
|
1890
|
+
status="ok",
|
|
1891
|
+
output=output,
|
|
1892
|
+
started_at=started_at,
|
|
1893
|
+
completed_at=_utc_timestamp(),
|
|
1894
|
+
duration_seconds=_duration_since(started_timer),
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def _run_tool_handler(
|
|
1899
|
+
tool: RuntimeToolSpec,
|
|
1900
|
+
input_payload: Dict[str, Any],
|
|
1901
|
+
) -> Dict[str, Any]:
|
|
1902
|
+
output = tool.handler(input_payload)
|
|
1903
|
+
if not isinstance(output, dict):
|
|
1904
|
+
raise ValueError("output must be an object")
|
|
1905
|
+
return output
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
def _note(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
1909
|
+
text = input_payload.get("text")
|
|
1910
|
+
if not isinstance(text, str):
|
|
1911
|
+
raise ValueError("text must be a string")
|
|
1912
|
+
return {"text": text}
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
def _read_file(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
1916
|
+
relative_path = input_payload.get("path")
|
|
1917
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
1918
|
+
raise ValueError("path must be a non-empty string")
|
|
1919
|
+
normalized_path = relative_path.strip()
|
|
1920
|
+
max_bytes = int(input_payload.get("max_bytes", _READ_FILE_MAX_BYTES))
|
|
1921
|
+
workspace_root = Path.cwd().resolve()
|
|
1922
|
+
target = _resolve_workspace_relative_path(workspace_root, normalized_path)
|
|
1923
|
+
if not target.exists():
|
|
1924
|
+
raise ValueError(f"file does not exist: {normalized_path}")
|
|
1925
|
+
if target.is_dir():
|
|
1926
|
+
raise ValueError(f"path is a directory: {normalized_path}")
|
|
1927
|
+
body = target.read_bytes()
|
|
1928
|
+
truncated = len(body) > max_bytes
|
|
1929
|
+
visible_body = body[:max_bytes] if truncated else body
|
|
1930
|
+
return {
|
|
1931
|
+
"path": _workspace_output_path(workspace_root, target),
|
|
1932
|
+
"content": visible_body.decode("utf-8", errors="replace"),
|
|
1933
|
+
"bytes": len(visible_body),
|
|
1934
|
+
"truncated": truncated,
|
|
1935
|
+
"sha256": hashlib.sha256(body).hexdigest(),
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
def _list_files(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
1940
|
+
relative_path = input_payload.get("path", ".")
|
|
1941
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
1942
|
+
raise ValueError("path must be a non-empty string")
|
|
1943
|
+
normalized_path = relative_path.strip()
|
|
1944
|
+
max_depth = int(input_payload.get("max_depth", _LIST_FILES_MAX_DEPTH))
|
|
1945
|
+
limit = int(input_payload.get("limit", _LIST_FILES_MAX_ENTRIES))
|
|
1946
|
+
workspace_root = Path.cwd().resolve()
|
|
1947
|
+
root = _resolve_workspace_relative_path(workspace_root, normalized_path)
|
|
1948
|
+
if not root.exists():
|
|
1949
|
+
raise ValueError(f"path does not exist: {normalized_path}")
|
|
1950
|
+
if root.is_file():
|
|
1951
|
+
entries = [_file_entry(workspace_root, root)]
|
|
1952
|
+
return {
|
|
1953
|
+
"root": _workspace_output_path(workspace_root, root),
|
|
1954
|
+
"entries": entries,
|
|
1955
|
+
"file_count": len(entries),
|
|
1956
|
+
"truncated": False,
|
|
1957
|
+
}
|
|
1958
|
+
entries = []
|
|
1959
|
+
truncated = False
|
|
1960
|
+
for path in _iter_workspace_entries(root, max_depth=max_depth):
|
|
1961
|
+
if len(entries) >= limit:
|
|
1962
|
+
truncated = True
|
|
1963
|
+
break
|
|
1964
|
+
entries.append(_file_entry(workspace_root, path))
|
|
1965
|
+
return {
|
|
1966
|
+
"root": _workspace_output_path(workspace_root, root),
|
|
1967
|
+
"entries": entries,
|
|
1968
|
+
"file_count": len(entries),
|
|
1969
|
+
"truncated": truncated,
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
def _workspace_write(
|
|
1974
|
+
input_payload: Dict[str, Any],
|
|
1975
|
+
*,
|
|
1976
|
+
runtime_workspace_dir: str = "",
|
|
1977
|
+
) -> Dict[str, Any]:
|
|
1978
|
+
kind = input_payload.get("kind")
|
|
1979
|
+
relative_path = input_payload.get("path")
|
|
1980
|
+
content = input_payload.get("content")
|
|
1981
|
+
metadata = input_payload.get("metadata", {})
|
|
1982
|
+
if not isinstance(kind, str):
|
|
1983
|
+
raise ValueError("kind must be a string")
|
|
1984
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
1985
|
+
raise ValueError("path must be a non-empty string")
|
|
1986
|
+
if not isinstance(content, str):
|
|
1987
|
+
raise ValueError("content must be a string")
|
|
1988
|
+
if not isinstance(metadata, dict):
|
|
1989
|
+
raise ValueError("metadata must be an object")
|
|
1990
|
+
return _runtime_workspace(runtime_workspace_dir).write_text(
|
|
1991
|
+
kind,
|
|
1992
|
+
relative_path.strip(),
|
|
1993
|
+
content,
|
|
1994
|
+
metadata=metadata,
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
def _workspace_read(
|
|
1999
|
+
input_payload: Dict[str, Any],
|
|
2000
|
+
*,
|
|
2001
|
+
runtime_workspace_dir: str = "",
|
|
2002
|
+
) -> Dict[str, Any]:
|
|
2003
|
+
kind = input_payload.get("kind")
|
|
2004
|
+
relative_path = input_payload.get("path")
|
|
2005
|
+
if not isinstance(kind, str):
|
|
2006
|
+
raise ValueError("kind must be a string")
|
|
2007
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
2008
|
+
raise ValueError("path must be a non-empty string")
|
|
2009
|
+
return _runtime_workspace(runtime_workspace_dir).read_text(
|
|
2010
|
+
kind,
|
|
2011
|
+
relative_path.strip(),
|
|
2012
|
+
max_bytes=int(input_payload.get("max_bytes", _READ_FILE_MAX_BYTES)),
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
def _workspace_list(
|
|
2017
|
+
input_payload: Dict[str, Any],
|
|
2018
|
+
*,
|
|
2019
|
+
runtime_workspace_dir: str = "",
|
|
2020
|
+
) -> Dict[str, Any]:
|
|
2021
|
+
kind = input_payload.get("kind")
|
|
2022
|
+
relative_path = input_payload.get("path", ".")
|
|
2023
|
+
if not isinstance(kind, str):
|
|
2024
|
+
raise ValueError("kind must be a string")
|
|
2025
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
2026
|
+
raise ValueError("path must be a non-empty string")
|
|
2027
|
+
return _runtime_workspace(runtime_workspace_dir).list(
|
|
2028
|
+
kind,
|
|
2029
|
+
relative_path.strip(),
|
|
2030
|
+
max_depth=int(input_payload.get("max_depth", _LIST_FILES_MAX_DEPTH)),
|
|
2031
|
+
limit=int(input_payload.get("limit", _LIST_FILES_MAX_ENTRIES)),
|
|
2032
|
+
)
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
def _workspace_history(
|
|
2036
|
+
input_payload: Dict[str, Any],
|
|
2037
|
+
*,
|
|
2038
|
+
runtime_workspace_dir: str = "",
|
|
2039
|
+
) -> Dict[str, Any]:
|
|
2040
|
+
kind = input_payload.get("kind")
|
|
2041
|
+
relative_path = input_payload.get("path")
|
|
2042
|
+
if not isinstance(kind, str):
|
|
2043
|
+
raise ValueError("kind must be a string")
|
|
2044
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
2045
|
+
raise ValueError("path must be a non-empty string")
|
|
2046
|
+
return _runtime_workspace(runtime_workspace_dir).history(
|
|
2047
|
+
kind,
|
|
2048
|
+
relative_path.strip(),
|
|
2049
|
+
limit=int(input_payload.get("limit", 20)),
|
|
2050
|
+
max_bytes=int(input_payload.get("max_bytes", _READ_FILE_MAX_BYTES)),
|
|
2051
|
+
)
|
|
2052
|
+
|
|
2053
|
+
|
|
2054
|
+
def _workspace_diff(
|
|
2055
|
+
input_payload: Dict[str, Any],
|
|
2056
|
+
*,
|
|
2057
|
+
runtime_workspace_dir: str = "",
|
|
2058
|
+
) -> Dict[str, Any]:
|
|
2059
|
+
kind = input_payload.get("kind")
|
|
2060
|
+
relative_path = input_payload.get("path")
|
|
2061
|
+
revision_id = input_payload.get("revision_id", "")
|
|
2062
|
+
if not isinstance(kind, str):
|
|
2063
|
+
raise ValueError("kind must be a string")
|
|
2064
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
2065
|
+
raise ValueError("path must be a non-empty string")
|
|
2066
|
+
if not isinstance(revision_id, str):
|
|
2067
|
+
raise ValueError("revision_id must be a string")
|
|
2068
|
+
return _runtime_workspace(runtime_workspace_dir).diff(
|
|
2069
|
+
kind,
|
|
2070
|
+
relative_path.strip(),
|
|
2071
|
+
revision_id=revision_id.strip(),
|
|
2072
|
+
context_lines=int(input_payload.get("context_lines", 3)),
|
|
2073
|
+
max_bytes=int(input_payload.get("max_bytes", _READ_FILE_MAX_BYTES)),
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
def _workspace_restore(
|
|
2078
|
+
input_payload: Dict[str, Any],
|
|
2079
|
+
*,
|
|
2080
|
+
runtime_workspace_dir: str = "",
|
|
2081
|
+
) -> Dict[str, Any]:
|
|
2082
|
+
kind = input_payload.get("kind")
|
|
2083
|
+
relative_path = input_payload.get("path")
|
|
2084
|
+
revision_id = input_payload.get("revision_id")
|
|
2085
|
+
expected_current_sha256 = input_payload.get("expected_current_sha256")
|
|
2086
|
+
expected_revision_sha256 = input_payload.get("expected_revision_sha256")
|
|
2087
|
+
if not isinstance(kind, str):
|
|
2088
|
+
raise ValueError("kind must be a string")
|
|
2089
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
2090
|
+
raise ValueError("path must be a non-empty string")
|
|
2091
|
+
if not isinstance(revision_id, str) or not revision_id.strip():
|
|
2092
|
+
raise ValueError("revision_id must be a non-empty string")
|
|
2093
|
+
if not isinstance(expected_current_sha256, str):
|
|
2094
|
+
raise ValueError("expected_current_sha256 must be a string")
|
|
2095
|
+
if not isinstance(expected_revision_sha256, str):
|
|
2096
|
+
raise ValueError("expected_revision_sha256 must be a string")
|
|
2097
|
+
return _runtime_workspace(runtime_workspace_dir).restore(
|
|
2098
|
+
kind,
|
|
2099
|
+
relative_path.strip(),
|
|
2100
|
+
revision_id=revision_id.strip(),
|
|
2101
|
+
expected_current_sha256=expected_current_sha256.strip(),
|
|
2102
|
+
expected_revision_sha256=expected_revision_sha256.strip(),
|
|
2103
|
+
)
|
|
2104
|
+
|
|
2105
|
+
|
|
2106
|
+
def _workspace_search(
|
|
2107
|
+
input_payload: Dict[str, Any],
|
|
2108
|
+
*,
|
|
2109
|
+
runtime_workspace_dir: str = "",
|
|
2110
|
+
) -> Dict[str, Any]:
|
|
2111
|
+
kind = input_payload.get("kind")
|
|
2112
|
+
query = input_payload.get("query")
|
|
2113
|
+
relative_path = input_payload.get("path", ".")
|
|
2114
|
+
if not isinstance(kind, str):
|
|
2115
|
+
raise ValueError("kind must be a string")
|
|
2116
|
+
if not isinstance(query, str) or not query:
|
|
2117
|
+
raise ValueError("query must be a non-empty string")
|
|
2118
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
2119
|
+
raise ValueError("path must be a non-empty string")
|
|
2120
|
+
return _runtime_workspace(runtime_workspace_dir).search(
|
|
2121
|
+
kind,
|
|
2122
|
+
query,
|
|
2123
|
+
relative_path.strip(),
|
|
2124
|
+
max_depth=int(input_payload.get("max_depth", _LIST_FILES_MAX_DEPTH)),
|
|
2125
|
+
limit=int(input_payload.get("limit", 50)),
|
|
2126
|
+
max_bytes=int(input_payload.get("max_bytes", _READ_FILE_MAX_BYTES)),
|
|
2127
|
+
)
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
def _runtime_workspace(runtime_workspace_dir: str = "") -> RuntimeWorkspace:
|
|
2131
|
+
if runtime_workspace_dir.strip():
|
|
2132
|
+
return RuntimeWorkspace(runtime_workspace_dir.strip())
|
|
2133
|
+
for env_var in _RUNTIME_WORKSPACE_DIR_ENV_VARS:
|
|
2134
|
+
configured = os.environ.get(env_var, "").strip()
|
|
2135
|
+
if configured:
|
|
2136
|
+
return RuntimeWorkspace(configured)
|
|
2137
|
+
return RuntimeWorkspace(Path.cwd() / ".kagent" / "runtime-workspace")
|
|
2138
|
+
|
|
2139
|
+
|
|
2140
|
+
def _skill_list(
|
|
2141
|
+
_input_payload: Dict[str, Any],
|
|
2142
|
+
*,
|
|
2143
|
+
runtime_skills_dir: str = "",
|
|
2144
|
+
) -> Dict[str, Any]:
|
|
2145
|
+
skills = _runtime_skill_registry(runtime_skills_dir).list_skills()
|
|
2146
|
+
return {"skills": skills, "skill_count": len(skills)}
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
def _skill_get(
|
|
2150
|
+
input_payload: Dict[str, Any],
|
|
2151
|
+
*,
|
|
2152
|
+
runtime_skills_dir: str = "",
|
|
2153
|
+
) -> Dict[str, Any]:
|
|
2154
|
+
name = input_payload.get("name")
|
|
2155
|
+
if not isinstance(name, str) or not name.strip():
|
|
2156
|
+
raise ValueError("name must be a non-empty string")
|
|
2157
|
+
return _runtime_skill_registry(runtime_skills_dir).get_skill(name.strip())
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
def _runtime_skill_registry(runtime_skills_dir: str = "") -> RuntimeSkillRegistry:
|
|
2161
|
+
if runtime_skills_dir.strip():
|
|
2162
|
+
return RuntimeSkillRegistry(runtime_skills_dir.strip())
|
|
2163
|
+
for env_var in _RUNTIME_SKILLS_DIR_ENV_VARS:
|
|
2164
|
+
configured = os.environ.get(env_var, "").strip()
|
|
2165
|
+
if configured:
|
|
2166
|
+
return RuntimeSkillRegistry(configured)
|
|
2167
|
+
return RuntimeSkillRegistry(Path.cwd() / ".kagent" / "skills")
|
|
2168
|
+
|
|
2169
|
+
|
|
2170
|
+
def _memory_put(
|
|
2171
|
+
input_payload: Dict[str, Any],
|
|
2172
|
+
*,
|
|
2173
|
+
redis_url: str,
|
|
2174
|
+
timeout_seconds: float,
|
|
2175
|
+
) -> Dict[str, Any]:
|
|
2176
|
+
if not redis_url:
|
|
2177
|
+
raise ValueError("redis memory backend is not configured")
|
|
2178
|
+
_reject_secret_like_memory_payload(input_payload.get("value"))
|
|
2179
|
+
ttl = input_payload.get("ttl_seconds", 0)
|
|
2180
|
+
if isinstance(ttl, bool) or not isinstance(ttl, (int, float)):
|
|
2181
|
+
raise ValueError("ttl_seconds must be a number")
|
|
2182
|
+
if int(ttl) != ttl:
|
|
2183
|
+
raise ValueError("ttl_seconds must be an integer")
|
|
2184
|
+
return RedisShortTermMemory(
|
|
2185
|
+
redis_url,
|
|
2186
|
+
timeout_seconds=timeout_seconds,
|
|
2187
|
+
).put(
|
|
2188
|
+
namespace=str(input_payload.get("namespace", "")),
|
|
2189
|
+
key=str(input_payload.get("key", "")),
|
|
2190
|
+
value=input_payload.get("value"),
|
|
2191
|
+
ttl_seconds=int(ttl),
|
|
2192
|
+
)
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
def _memory_get(
|
|
2196
|
+
input_payload: Dict[str, Any],
|
|
2197
|
+
*,
|
|
2198
|
+
redis_url: str,
|
|
2199
|
+
timeout_seconds: float,
|
|
2200
|
+
) -> Dict[str, Any]:
|
|
2201
|
+
if not redis_url:
|
|
2202
|
+
raise ValueError("redis memory backend is not configured")
|
|
2203
|
+
return RedisShortTermMemory(
|
|
2204
|
+
redis_url,
|
|
2205
|
+
timeout_seconds=timeout_seconds,
|
|
2206
|
+
).get(
|
|
2207
|
+
namespace=str(input_payload.get("namespace", "")),
|
|
2208
|
+
key=str(input_payload.get("key", "")),
|
|
2209
|
+
)
|
|
2210
|
+
|
|
2211
|
+
|
|
2212
|
+
def _memory_upsert(
|
|
2213
|
+
input_payload: Dict[str, Any],
|
|
2214
|
+
*,
|
|
2215
|
+
milvus_url: str,
|
|
2216
|
+
timeout_seconds: float,
|
|
2217
|
+
) -> Dict[str, Any]:
|
|
2218
|
+
if not milvus_url:
|
|
2219
|
+
raise ValueError("milvus memory backend is not configured")
|
|
2220
|
+
metadata = input_payload.get("metadata", {})
|
|
2221
|
+
if not isinstance(metadata, dict):
|
|
2222
|
+
raise ValueError("metadata must be an object")
|
|
2223
|
+
_reject_secret_like_memory_payload(
|
|
2224
|
+
{
|
|
2225
|
+
"text": input_payload.get("text", ""),
|
|
2226
|
+
"metadata": metadata,
|
|
2227
|
+
}
|
|
2228
|
+
)
|
|
2229
|
+
return MilvusLongTermMemory(
|
|
2230
|
+
milvus_url,
|
|
2231
|
+
timeout_seconds=timeout_seconds,
|
|
2232
|
+
).upsert(
|
|
2233
|
+
collection=str(input_payload.get("collection", "")),
|
|
2234
|
+
memory_id=str(input_payload.get("memory_id", "")),
|
|
2235
|
+
text=str(input_payload.get("text", "")),
|
|
2236
|
+
vector=input_payload.get("vector", []),
|
|
2237
|
+
metadata=metadata,
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
|
|
2241
|
+
def _memory_search(
|
|
2242
|
+
input_payload: Dict[str, Any],
|
|
2243
|
+
*,
|
|
2244
|
+
milvus_url: str,
|
|
2245
|
+
timeout_seconds: float,
|
|
2246
|
+
) -> Dict[str, Any]:
|
|
2247
|
+
if not milvus_url:
|
|
2248
|
+
raise ValueError("milvus memory backend is not configured")
|
|
2249
|
+
limit = input_payload.get("limit", 5)
|
|
2250
|
+
if isinstance(limit, bool) or not isinstance(limit, (int, float)):
|
|
2251
|
+
raise ValueError("limit must be a number")
|
|
2252
|
+
if int(limit) != limit:
|
|
2253
|
+
raise ValueError("limit must be an integer")
|
|
2254
|
+
return MilvusLongTermMemory(
|
|
2255
|
+
milvus_url,
|
|
2256
|
+
timeout_seconds=timeout_seconds,
|
|
2257
|
+
).search(
|
|
2258
|
+
collection=str(input_payload.get("collection", "")),
|
|
2259
|
+
vector=input_payload.get("vector", []),
|
|
2260
|
+
limit=int(limit),
|
|
2261
|
+
)
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
def _memory_remember(
|
|
2265
|
+
input_payload: Dict[str, Any],
|
|
2266
|
+
*,
|
|
2267
|
+
milvus_url: str,
|
|
2268
|
+
embedding_config: EmbeddingProviderConfig,
|
|
2269
|
+
timeout_seconds: float,
|
|
2270
|
+
) -> Dict[str, Any]:
|
|
2271
|
+
if not milvus_url:
|
|
2272
|
+
raise ValueError("milvus memory backend is not configured")
|
|
2273
|
+
_require_embedding_config(embedding_config)
|
|
2274
|
+
metadata = input_payload.get("metadata", {})
|
|
2275
|
+
if not isinstance(metadata, dict):
|
|
2276
|
+
raise ValueError("metadata must be an object")
|
|
2277
|
+
text = str(input_payload.get("text", ""))
|
|
2278
|
+
_reject_secret_like_memory_payload({"text": text, "metadata": metadata})
|
|
2279
|
+
vector = OpenAICompatibleEmbeddingProvider(embedding_config).embed(text)
|
|
2280
|
+
output = MilvusLongTermMemory(
|
|
2281
|
+
milvus_url,
|
|
2282
|
+
timeout_seconds=timeout_seconds,
|
|
2283
|
+
).upsert(
|
|
2284
|
+
collection=str(input_payload.get("collection", "")),
|
|
2285
|
+
memory_id=str(input_payload.get("memory_id", "")),
|
|
2286
|
+
text=text,
|
|
2287
|
+
vector=vector,
|
|
2288
|
+
metadata=metadata,
|
|
2289
|
+
)
|
|
2290
|
+
return {
|
|
2291
|
+
**output,
|
|
2292
|
+
"embedding_model": embedding_config.model,
|
|
2293
|
+
"vector_dimensions": str(len(vector)),
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
|
|
2297
|
+
def _memory_recall(
|
|
2298
|
+
input_payload: Dict[str, Any],
|
|
2299
|
+
*,
|
|
2300
|
+
milvus_url: str,
|
|
2301
|
+
embedding_config: EmbeddingProviderConfig,
|
|
2302
|
+
timeout_seconds: float,
|
|
2303
|
+
) -> Dict[str, Any]:
|
|
2304
|
+
if not milvus_url:
|
|
2305
|
+
raise ValueError("milvus memory backend is not configured")
|
|
2306
|
+
_require_embedding_config(embedding_config)
|
|
2307
|
+
limit = input_payload.get("limit", 5)
|
|
2308
|
+
if isinstance(limit, bool) or not isinstance(limit, (int, float)):
|
|
2309
|
+
raise ValueError("limit must be a number")
|
|
2310
|
+
if int(limit) != limit:
|
|
2311
|
+
raise ValueError("limit must be an integer")
|
|
2312
|
+
vector = OpenAICompatibleEmbeddingProvider(embedding_config).embed(
|
|
2313
|
+
str(input_payload.get("query", ""))
|
|
2314
|
+
)
|
|
2315
|
+
output = MilvusLongTermMemory(
|
|
2316
|
+
milvus_url,
|
|
2317
|
+
timeout_seconds=timeout_seconds,
|
|
2318
|
+
).search(
|
|
2319
|
+
collection=str(input_payload.get("collection", "")),
|
|
2320
|
+
vector=vector,
|
|
2321
|
+
limit=int(limit),
|
|
2322
|
+
)
|
|
2323
|
+
return {
|
|
2324
|
+
**output,
|
|
2325
|
+
"embedding_model": embedding_config.model,
|
|
2326
|
+
"vector_dimensions": str(len(vector)),
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
|
|
2330
|
+
def _require_embedding_config(config: EmbeddingProviderConfig) -> None:
|
|
2331
|
+
if not config.base_url:
|
|
2332
|
+
raise ValueError("embedding base_url is not configured")
|
|
2333
|
+
if not config.model:
|
|
2334
|
+
raise ValueError("embedding model is not configured")
|
|
2335
|
+
|
|
2336
|
+
|
|
2337
|
+
def _reject_secret_like_memory_payload(value: Any) -> None:
|
|
2338
|
+
if redact_runtime_payload(value) != value:
|
|
2339
|
+
raise ValueError("memory write payload contains secret-like text")
|
|
2340
|
+
|
|
2341
|
+
|
|
2342
|
+
def _first_env_value(names: tuple[str, ...]) -> str:
|
|
2343
|
+
for name in names:
|
|
2344
|
+
value = os.environ.get(name, "").strip()
|
|
2345
|
+
if value:
|
|
2346
|
+
return value
|
|
2347
|
+
return ""
|
|
2348
|
+
|
|
2349
|
+
|
|
2350
|
+
def _embedding_config(
|
|
2351
|
+
*,
|
|
2352
|
+
base_url: str,
|
|
2353
|
+
api_key: str,
|
|
2354
|
+
model: str,
|
|
2355
|
+
timeout_seconds: float,
|
|
2356
|
+
max_retries: int,
|
|
2357
|
+
retry_backoff_seconds: float,
|
|
2358
|
+
) -> EmbeddingProviderConfig:
|
|
2359
|
+
return EmbeddingProviderConfig(
|
|
2360
|
+
base_url=base_url.strip() or _first_env_value(_EMBEDDING_BASE_URL_ENV_VARS),
|
|
2361
|
+
api_key=api_key.strip() or _first_env_value(_EMBEDDING_API_KEY_ENV_VARS),
|
|
2362
|
+
model=model.strip() or _first_env_value(_EMBEDDING_MODEL_ENV_VARS),
|
|
2363
|
+
timeout_seconds=_embedding_timeout_seconds(timeout_seconds),
|
|
2364
|
+
max_retries=_embedding_max_retries(max_retries),
|
|
2365
|
+
retry_backoff_seconds=_embedding_retry_backoff_seconds(
|
|
2366
|
+
retry_backoff_seconds
|
|
2367
|
+
),
|
|
2368
|
+
)
|
|
2369
|
+
|
|
2370
|
+
|
|
2371
|
+
def _embedding_timeout_seconds(default_value: float) -> float:
|
|
2372
|
+
env_value = os.environ.get("KAGENT_EMBEDDING_TIMEOUT_SECONDS", "").strip()
|
|
2373
|
+
value = env_value or str(default_value)
|
|
2374
|
+
try:
|
|
2375
|
+
timeout = float(value)
|
|
2376
|
+
except ValueError as exc:
|
|
2377
|
+
raise ValueError("embedding timeout must be a float") from exc
|
|
2378
|
+
if timeout <= 0:
|
|
2379
|
+
raise ValueError("embedding timeout must be positive")
|
|
2380
|
+
return timeout
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def _embedding_max_retries(default_value: int) -> int:
|
|
2384
|
+
env_value = os.environ.get(_EMBEDDING_MAX_RETRIES_ENV_VAR, "").strip()
|
|
2385
|
+
value = env_value or str(default_value)
|
|
2386
|
+
try:
|
|
2387
|
+
retries = int(value)
|
|
2388
|
+
except ValueError as exc:
|
|
2389
|
+
raise ValueError("embedding max_retries must be an integer") from exc
|
|
2390
|
+
if retries < 0:
|
|
2391
|
+
raise ValueError("embedding max_retries must be non-negative")
|
|
2392
|
+
return retries
|
|
2393
|
+
|
|
2394
|
+
|
|
2395
|
+
def _embedding_retry_backoff_seconds(default_value: float) -> float:
|
|
2396
|
+
env_value = os.environ.get(_EMBEDDING_RETRY_BACKOFF_ENV_VAR, "").strip()
|
|
2397
|
+
value = env_value or str(default_value)
|
|
2398
|
+
try:
|
|
2399
|
+
backoff = float(value)
|
|
2400
|
+
except ValueError as exc:
|
|
2401
|
+
raise ValueError("embedding retry_backoff_seconds must be a float") from exc
|
|
2402
|
+
if backoff < 0:
|
|
2403
|
+
raise ValueError("embedding retry_backoff_seconds must be non-negative")
|
|
2404
|
+
return backoff
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
def _backend_timeout_seconds(default_value: float) -> float:
|
|
2408
|
+
env_value = os.environ.get(_EXTERNAL_BACKEND_TIMEOUT_ENV_VAR, "").strip()
|
|
2409
|
+
value = env_value or str(default_value)
|
|
2410
|
+
try:
|
|
2411
|
+
timeout = float(value)
|
|
2412
|
+
except ValueError as exc:
|
|
2413
|
+
raise ValueError("external backend timeout must be a float") from exc
|
|
2414
|
+
if timeout <= 0:
|
|
2415
|
+
raise ValueError("external backend timeout must be positive")
|
|
2416
|
+
return timeout
|
|
2417
|
+
|
|
2418
|
+
|
|
2419
|
+
def _shell_command(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
2420
|
+
command = input_payload.get("command")
|
|
2421
|
+
if not isinstance(command, str) or not command.strip():
|
|
2422
|
+
raise ValueError("command must be a non-empty string")
|
|
2423
|
+
normalized_command = command.strip()
|
|
2424
|
+
_validate_shell_command(normalized_command)
|
|
2425
|
+
|
|
2426
|
+
timeout_seconds = float(
|
|
2427
|
+
input_payload.get("timeout_seconds", _SHELL_COMMAND_DEFAULT_TIMEOUT_SECONDS)
|
|
2428
|
+
)
|
|
2429
|
+
max_output_bytes = int(
|
|
2430
|
+
input_payload.get("max_output_bytes", _SHELL_COMMAND_MAX_OUTPUT_BYTES)
|
|
2431
|
+
)
|
|
2432
|
+
workspace_root = Path.cwd().resolve()
|
|
2433
|
+
cwd = _resolve_shell_cwd(workspace_root, input_payload.get("cwd", "."))
|
|
2434
|
+
sandbox_env = _shell_sandbox_env(workspace_root, cwd)
|
|
2435
|
+
sandbox = _shell_sandbox_metadata()
|
|
2436
|
+
started = time.perf_counter()
|
|
2437
|
+
timed_out = False
|
|
2438
|
+
try:
|
|
2439
|
+
sandbox_result = run_shell_sandboxed(
|
|
2440
|
+
normalized_command,
|
|
2441
|
+
workspace_root=workspace_root,
|
|
2442
|
+
cwd=cwd,
|
|
2443
|
+
env=sandbox_env,
|
|
2444
|
+
timeout_seconds=timeout_seconds,
|
|
2445
|
+
)
|
|
2446
|
+
sandbox = sandbox_result.metadata
|
|
2447
|
+
completed = sandbox_result.completed
|
|
2448
|
+
exit_code = completed.returncode
|
|
2449
|
+
stdout = completed.stdout or b""
|
|
2450
|
+
stderr = completed.stderr or b""
|
|
2451
|
+
except subprocess.TimeoutExpired as exc:
|
|
2452
|
+
timed_out = True
|
|
2453
|
+
exit_code = -1
|
|
2454
|
+
stdout = exc.stdout or b""
|
|
2455
|
+
stderr = exc.stderr or b""
|
|
2456
|
+
duration_seconds = time.perf_counter() - started
|
|
2457
|
+
stdout, stderr, truncated = _truncate_shell_output(
|
|
2458
|
+
stdout,
|
|
2459
|
+
stderr,
|
|
2460
|
+
max_output_bytes=max_output_bytes,
|
|
2461
|
+
)
|
|
2462
|
+
return {
|
|
2463
|
+
"command": normalized_command,
|
|
2464
|
+
"cwd": _workspace_output_path(workspace_root, cwd),
|
|
2465
|
+
"sandbox": sandbox,
|
|
2466
|
+
"exit_code": exit_code,
|
|
2467
|
+
"stdout": stdout.decode("utf-8", errors="replace"),
|
|
2468
|
+
"stderr": stderr.decode("utf-8", errors="replace"),
|
|
2469
|
+
"duration_seconds": float(f"{duration_seconds:.4f}"),
|
|
2470
|
+
"timed_out": timed_out,
|
|
2471
|
+
"truncated": truncated,
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
|
|
2475
|
+
def _validate_shell_command(command: str) -> None:
|
|
2476
|
+
if "\x00" in command or "\n" in command or "\r" in command:
|
|
2477
|
+
raise ValueError("command must be a single line")
|
|
2478
|
+
if _SHELL_BACKGROUND_PATTERN.search(command):
|
|
2479
|
+
raise ValueError("background shell commands are not supported")
|
|
2480
|
+
for pattern in _SHELL_INTERACTIVE_PATTERNS:
|
|
2481
|
+
if pattern.search(command):
|
|
2482
|
+
raise ValueError("interactive shell commands are not supported")
|
|
2483
|
+
for pattern in _SHELL_DESTRUCTIVE_PATTERNS:
|
|
2484
|
+
if pattern.search(command):
|
|
2485
|
+
raise ValueError("high-risk shell commands are not supported")
|
|
2486
|
+
for pattern in _SHELL_SECRET_EXPOSURE_PATTERNS:
|
|
2487
|
+
if pattern.search(command):
|
|
2488
|
+
raise ValueError("secret-exposing shell commands are not supported")
|
|
2489
|
+
if _SHELL_PIPE_TO_SHELL_PATTERN.search(command):
|
|
2490
|
+
raise ValueError("pipe-to-shell commands are not supported")
|
|
2491
|
+
for pattern in _SHELL_NETWORK_COMMAND_PATTERNS:
|
|
2492
|
+
if pattern.search(command):
|
|
2493
|
+
raise ValueError("network shell commands are not supported; use http_request")
|
|
2494
|
+
for pattern in _SHELL_INLINE_NETWORK_CODE_PATTERNS:
|
|
2495
|
+
if pattern.search(command):
|
|
2496
|
+
raise ValueError("network shell commands are not supported; use http_request")
|
|
2497
|
+
|
|
2498
|
+
|
|
2499
|
+
def _shell_sandbox_metadata() -> Dict[str, str]:
|
|
2500
|
+
return {
|
|
2501
|
+
"enabled": "true",
|
|
2502
|
+
"backend": "soft",
|
|
2503
|
+
"enforced": "false",
|
|
2504
|
+
"filesystem": "workspace",
|
|
2505
|
+
"network": "disabled",
|
|
2506
|
+
"env_policy": "minimal",
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
|
|
2510
|
+
def _shell_sandbox_env(workspace_root: Path, cwd: Path) -> Dict[str, str]:
|
|
2511
|
+
return {
|
|
2512
|
+
"HOME": str(workspace_root),
|
|
2513
|
+
"LANG": "C.UTF-8",
|
|
2514
|
+
"LC_ALL": "C.UTF-8",
|
|
2515
|
+
"PATH": os.environ.get("PATH", os.defpath),
|
|
2516
|
+
"PWD": str(cwd),
|
|
2517
|
+
"KAGENT_RUNTIME_SANDBOX": "1",
|
|
2518
|
+
"KAGENT_SANDBOX_FILESYSTEM": "workspace",
|
|
2519
|
+
"KAGENT_SANDBOX_NETWORK": "disabled",
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
|
|
2523
|
+
def _resolve_shell_cwd(workspace_root: Path, relative_path: Any) -> Path:
|
|
2524
|
+
if relative_path in {"", "."}:
|
|
2525
|
+
return workspace_root
|
|
2526
|
+
if not isinstance(relative_path, str) or not relative_path.strip():
|
|
2527
|
+
raise ValueError("cwd must be a string")
|
|
2528
|
+
cwd = _resolve_workspace_relative_path(workspace_root, relative_path.strip())
|
|
2529
|
+
if not cwd.exists():
|
|
2530
|
+
raise ValueError(f"cwd does not exist: {relative_path}")
|
|
2531
|
+
if not cwd.is_dir():
|
|
2532
|
+
raise ValueError(f"cwd is not a directory: {relative_path}")
|
|
2533
|
+
return cwd
|
|
2534
|
+
|
|
2535
|
+
|
|
2536
|
+
def _truncate_shell_output(
|
|
2537
|
+
stdout: bytes,
|
|
2538
|
+
stderr: bytes,
|
|
2539
|
+
*,
|
|
2540
|
+
max_output_bytes: int,
|
|
2541
|
+
) -> tuple[bytes, bytes, bool]:
|
|
2542
|
+
remaining = max_output_bytes
|
|
2543
|
+
truncated = False
|
|
2544
|
+
if len(stdout) > remaining:
|
|
2545
|
+
stdout = stdout[:remaining]
|
|
2546
|
+
remaining = 0
|
|
2547
|
+
truncated = True
|
|
2548
|
+
else:
|
|
2549
|
+
remaining -= len(stdout)
|
|
2550
|
+
if len(stderr) > remaining:
|
|
2551
|
+
stderr = stderr[:remaining]
|
|
2552
|
+
truncated = True
|
|
2553
|
+
return stdout, stderr, truncated
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
def _apply_patch(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
2557
|
+
patch = input_payload.get("patch")
|
|
2558
|
+
if not isinstance(patch, str) or not patch.strip():
|
|
2559
|
+
raise ValueError("patch must be a non-empty string")
|
|
2560
|
+
operations = _parse_workspace_patch(patch)
|
|
2561
|
+
workspace_root = Path.cwd().resolve()
|
|
2562
|
+
store = PatchCheckpointStore.from_environment()
|
|
2563
|
+
store.recover_prepared(workspace_root)
|
|
2564
|
+
with workspace_transaction(workspace_root):
|
|
2565
|
+
return _stage_and_commit_workspace_patch(workspace_root, operations, store)
|
|
2566
|
+
|
|
2567
|
+
|
|
2568
|
+
def _stage_and_commit_workspace_patch(
|
|
2569
|
+
workspace_root: Path,
|
|
2570
|
+
operations: list[_PatchOperation],
|
|
2571
|
+
store: PatchCheckpointStore,
|
|
2572
|
+
) -> Dict[str, Any]:
|
|
2573
|
+
staged_contents: dict[Path, str | None] = {}
|
|
2574
|
+
changed_files = []
|
|
2575
|
+
|
|
2576
|
+
for operation in operations:
|
|
2577
|
+
target = _resolve_workspace_relative_path(workspace_root, operation.relative_path)
|
|
2578
|
+
current_content = _staged_or_disk_content(target, staged_contents)
|
|
2579
|
+
if operation.operation == "add":
|
|
2580
|
+
if current_content is not None:
|
|
2581
|
+
raise ValueError(f"file already exists: {operation.relative_path}")
|
|
2582
|
+
next_content: str | None = operation.content
|
|
2583
|
+
elif operation.operation == "update":
|
|
2584
|
+
if current_content is None:
|
|
2585
|
+
raise ValueError(f"file does not exist: {operation.relative_path}")
|
|
2586
|
+
next_content = _apply_update_lines(
|
|
2587
|
+
current_content,
|
|
2588
|
+
operation.lines,
|
|
2589
|
+
operation.relative_path,
|
|
2590
|
+
)
|
|
2591
|
+
elif operation.operation == "delete":
|
|
2592
|
+
if current_content is None:
|
|
2593
|
+
raise ValueError(f"file does not exist: {operation.relative_path}")
|
|
2594
|
+
if target.is_dir():
|
|
2595
|
+
raise ValueError(f"path is a directory: {operation.relative_path}")
|
|
2596
|
+
next_content = None
|
|
2597
|
+
elif operation.operation == "move":
|
|
2598
|
+
if current_content is None:
|
|
2599
|
+
raise ValueError(f"file does not exist: {operation.relative_path}")
|
|
2600
|
+
if target.is_dir():
|
|
2601
|
+
raise ValueError(f"path is a directory: {operation.relative_path}")
|
|
2602
|
+
destination = _resolve_workspace_relative_path(
|
|
2603
|
+
workspace_root,
|
|
2604
|
+
operation.destination_path,
|
|
2605
|
+
)
|
|
2606
|
+
if destination == target:
|
|
2607
|
+
raise ValueError("move destination must differ from source")
|
|
2608
|
+
if _staged_or_disk_content(destination, staged_contents) is not None:
|
|
2609
|
+
raise ValueError(f"file already exists: {operation.destination_path}")
|
|
2610
|
+
next_content = current_content
|
|
2611
|
+
if operation.lines:
|
|
2612
|
+
next_content = _apply_update_lines(
|
|
2613
|
+
current_content,
|
|
2614
|
+
operation.lines,
|
|
2615
|
+
operation.relative_path,
|
|
2616
|
+
)
|
|
2617
|
+
staged_contents[target] = None
|
|
2618
|
+
staged_contents[destination] = next_content
|
|
2619
|
+
encoded = next_content.encode("utf-8")
|
|
2620
|
+
changed_files.append(
|
|
2621
|
+
{
|
|
2622
|
+
"path": operation.destination_path,
|
|
2623
|
+
"previous_path": operation.relative_path,
|
|
2624
|
+
"operation": operation.operation,
|
|
2625
|
+
"bytes": len(encoded),
|
|
2626
|
+
"sha256": hashlib.sha256(encoded).hexdigest(),
|
|
2627
|
+
}
|
|
2628
|
+
)
|
|
2629
|
+
continue
|
|
2630
|
+
else: # pragma: no cover - parser owns the operation enum
|
|
2631
|
+
raise ValueError(f"unsupported patch operation: {operation.operation}")
|
|
2632
|
+
|
|
2633
|
+
staged_contents[target] = next_content
|
|
2634
|
+
encoded = (next_content or "").encode("utf-8")
|
|
2635
|
+
changed_files.append(
|
|
2636
|
+
{
|
|
2637
|
+
"path": operation.relative_path,
|
|
2638
|
+
"operation": operation.operation,
|
|
2639
|
+
"bytes": len(encoded),
|
|
2640
|
+
"sha256": hashlib.sha256(encoded).hexdigest(),
|
|
2641
|
+
}
|
|
2642
|
+
)
|
|
2643
|
+
|
|
2644
|
+
commit_checkpointed_text_changes(
|
|
2645
|
+
store,
|
|
2646
|
+
workspace_root,
|
|
2647
|
+
staged_contents,
|
|
2648
|
+
)
|
|
2649
|
+
return {"changed_files": changed_files, "file_count": len(changed_files)}
|
|
2650
|
+
|
|
2651
|
+
|
|
2652
|
+
def _patch_history(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
2653
|
+
limit = int(input_payload.get("limit", 20))
|
|
2654
|
+
workspace_root = Path.cwd().resolve()
|
|
2655
|
+
store = PatchCheckpointStore.from_environment()
|
|
2656
|
+
store.recover_prepared(workspace_root)
|
|
2657
|
+
return store.history(workspace_root, limit=limit)
|
|
2658
|
+
|
|
2659
|
+
|
|
2660
|
+
def _revert_patch(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
2661
|
+
checkpoint_id = input_payload.get("checkpoint_id")
|
|
2662
|
+
paths = input_payload.get("paths")
|
|
2663
|
+
if not isinstance(checkpoint_id, str) or not checkpoint_id.strip():
|
|
2664
|
+
raise ValueError("checkpoint_id must be a non-empty string")
|
|
2665
|
+
if not isinstance(paths, list) or not all(
|
|
2666
|
+
isinstance(path, str) and path for path in paths
|
|
2667
|
+
):
|
|
2668
|
+
raise ValueError("paths must be a non-empty string array")
|
|
2669
|
+
workspace_root = Path.cwd().resolve()
|
|
2670
|
+
store = PatchCheckpointStore.from_environment()
|
|
2671
|
+
store.recover_prepared(workspace_root)
|
|
2672
|
+
with workspace_transaction(workspace_root):
|
|
2673
|
+
revert = store.load_revert(workspace_root, checkpoint_id.strip())
|
|
2674
|
+
checkpoint_paths = [
|
|
2675
|
+
target.relative_to(workspace_root).as_posix()
|
|
2676
|
+
for target in revert.staged_contents
|
|
2677
|
+
]
|
|
2678
|
+
if paths != checkpoint_paths:
|
|
2679
|
+
raise ValueError("paths do not match the reviewed checkpoint")
|
|
2680
|
+
for target in revert.staged_contents:
|
|
2681
|
+
_reject_symlink_path_parts(
|
|
2682
|
+
workspace_root,
|
|
2683
|
+
target.relative_to(workspace_root),
|
|
2684
|
+
)
|
|
2685
|
+
current_states = capture_text_states(revert.expected_current)
|
|
2686
|
+
for target, expected_content in revert.expected_current.items():
|
|
2687
|
+
if current_states[target].content != expected_content:
|
|
2688
|
+
relative_path = target.relative_to(workspace_root).as_posix()
|
|
2689
|
+
raise ValueError(
|
|
2690
|
+
f"current SHA-256 does not match checkpoint: {relative_path}"
|
|
2691
|
+
)
|
|
2692
|
+
new_checkpoint_id = commit_checkpointed_text_changes(
|
|
2693
|
+
store,
|
|
2694
|
+
workspace_root,
|
|
2695
|
+
revert.staged_contents,
|
|
2696
|
+
target_modes=revert.target_modes,
|
|
2697
|
+
)
|
|
2698
|
+
changed_files = []
|
|
2699
|
+
for target, content in revert.staged_contents.items():
|
|
2700
|
+
previous_content = revert.expected_current[target]
|
|
2701
|
+
if previous_content is None:
|
|
2702
|
+
operation = "add"
|
|
2703
|
+
elif content is None:
|
|
2704
|
+
operation = "delete"
|
|
2705
|
+
else:
|
|
2706
|
+
operation = "update"
|
|
2707
|
+
encoded = (content or "").encode("utf-8")
|
|
2708
|
+
changed_files.append(
|
|
2709
|
+
{
|
|
2710
|
+
"path": target.relative_to(workspace_root).as_posix(),
|
|
2711
|
+
"operation": operation,
|
|
2712
|
+
"bytes": len(encoded),
|
|
2713
|
+
"sha256": hashlib.sha256(encoded).hexdigest(),
|
|
2714
|
+
}
|
|
2715
|
+
)
|
|
2716
|
+
return {
|
|
2717
|
+
"checkpoint_id": new_checkpoint_id,
|
|
2718
|
+
"reverted_checkpoint_id": checkpoint_id.strip(),
|
|
2719
|
+
"changed_files": changed_files,
|
|
2720
|
+
"file_count": len(changed_files),
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
|
|
2724
|
+
def _parse_workspace_patch(patch: str) -> list[_PatchOperation]:
|
|
2725
|
+
lines = patch.splitlines()
|
|
2726
|
+
if not lines or lines[0] != "*** Begin Patch":
|
|
2727
|
+
raise ValueError("patch must start with *** Begin Patch")
|
|
2728
|
+
if lines[-1] != "*** End Patch":
|
|
2729
|
+
raise ValueError("patch must end with *** End Patch")
|
|
2730
|
+
operations = []
|
|
2731
|
+
index = 1
|
|
2732
|
+
while index < len(lines) - 1:
|
|
2733
|
+
line = lines[index]
|
|
2734
|
+
if line.startswith("*** Add File: "):
|
|
2735
|
+
relative_path = line.removeprefix("*** Add File: ").strip()
|
|
2736
|
+
if not relative_path:
|
|
2737
|
+
raise ValueError("patch file path is required")
|
|
2738
|
+
index += 1
|
|
2739
|
+
content_lines = []
|
|
2740
|
+
while index < len(lines) - 1 and not lines[index].startswith("*** "):
|
|
2741
|
+
content_line = lines[index]
|
|
2742
|
+
content_lines.append(
|
|
2743
|
+
content_line[1:] if content_line.startswith("+") else content_line
|
|
2744
|
+
)
|
|
2745
|
+
index += 1
|
|
2746
|
+
if not content_lines:
|
|
2747
|
+
raise ValueError("add file patch must contain at least one content line")
|
|
2748
|
+
operations.append(
|
|
2749
|
+
_PatchOperation(
|
|
2750
|
+
operation="add",
|
|
2751
|
+
relative_path=relative_path,
|
|
2752
|
+
content="\n".join(content_lines) + "\n",
|
|
2753
|
+
)
|
|
2754
|
+
)
|
|
2755
|
+
continue
|
|
2756
|
+
if line.startswith("*** Update File: "):
|
|
2757
|
+
relative_path = line.removeprefix("*** Update File: ").strip()
|
|
2758
|
+
if not relative_path:
|
|
2759
|
+
raise ValueError("patch file path is required")
|
|
2760
|
+
index += 1
|
|
2761
|
+
destination_path = ""
|
|
2762
|
+
if index < len(lines) - 1 and lines[index].startswith("*** Move to: "):
|
|
2763
|
+
destination_path = lines[index].removeprefix("*** Move to: ").strip()
|
|
2764
|
+
if not destination_path:
|
|
2765
|
+
raise ValueError("move destination path is required")
|
|
2766
|
+
index += 1
|
|
2767
|
+
update_lines = []
|
|
2768
|
+
while index < len(lines) - 1 and not lines[index].startswith("*** "):
|
|
2769
|
+
update_line = lines[index]
|
|
2770
|
+
if update_line == "@@":
|
|
2771
|
+
index += 1
|
|
2772
|
+
continue
|
|
2773
|
+
update_lines.append(update_line)
|
|
2774
|
+
index += 1
|
|
2775
|
+
if not update_lines and not destination_path:
|
|
2776
|
+
raise ValueError("update file patch must contain at least one change line")
|
|
2777
|
+
if destination_path:
|
|
2778
|
+
operations.append(
|
|
2779
|
+
_PatchOperation(
|
|
2780
|
+
operation="move",
|
|
2781
|
+
relative_path=relative_path,
|
|
2782
|
+
destination_path=destination_path,
|
|
2783
|
+
lines=tuple(update_lines),
|
|
2784
|
+
)
|
|
2785
|
+
)
|
|
2786
|
+
continue
|
|
2787
|
+
operations.append(
|
|
2788
|
+
_PatchOperation(
|
|
2789
|
+
operation="update",
|
|
2790
|
+
relative_path=relative_path,
|
|
2791
|
+
lines=tuple(update_lines),
|
|
2792
|
+
)
|
|
2793
|
+
)
|
|
2794
|
+
continue
|
|
2795
|
+
if line.startswith("*** Delete File: "):
|
|
2796
|
+
relative_path = line.removeprefix("*** Delete File: ").strip()
|
|
2797
|
+
if not relative_path:
|
|
2798
|
+
raise ValueError("patch file path is required")
|
|
2799
|
+
index += 1
|
|
2800
|
+
operations.append(
|
|
2801
|
+
_PatchOperation(operation="delete", relative_path=relative_path)
|
|
2802
|
+
)
|
|
2803
|
+
continue
|
|
2804
|
+
raise ValueError(
|
|
2805
|
+
"only Add File, Update File, Move to, and Delete File patch hunks are supported"
|
|
2806
|
+
)
|
|
2807
|
+
if not operations:
|
|
2808
|
+
raise ValueError("patch must contain at least one file hunk")
|
|
2809
|
+
return operations
|
|
2810
|
+
|
|
2811
|
+
|
|
2812
|
+
def _staged_or_disk_content(
|
|
2813
|
+
target: Path,
|
|
2814
|
+
staged_contents: dict[Path, str | None],
|
|
2815
|
+
) -> str | None:
|
|
2816
|
+
if target in staged_contents:
|
|
2817
|
+
return staged_contents[target]
|
|
2818
|
+
if not target.exists():
|
|
2819
|
+
return None
|
|
2820
|
+
if target.is_dir():
|
|
2821
|
+
raise ValueError("path is a directory")
|
|
2822
|
+
return target.read_text(encoding="utf-8")
|
|
2823
|
+
|
|
2824
|
+
|
|
2825
|
+
def _apply_update_lines(
|
|
2826
|
+
content: str,
|
|
2827
|
+
update_lines: tuple[str, ...],
|
|
2828
|
+
relative_path: str,
|
|
2829
|
+
) -> str:
|
|
2830
|
+
old_lines = []
|
|
2831
|
+
new_lines = []
|
|
2832
|
+
for line in update_lines:
|
|
2833
|
+
if line.startswith(" "):
|
|
2834
|
+
value = line[1:]
|
|
2835
|
+
old_lines.append(value)
|
|
2836
|
+
new_lines.append(value)
|
|
2837
|
+
continue
|
|
2838
|
+
if line.startswith("-"):
|
|
2839
|
+
old_lines.append(line[1:])
|
|
2840
|
+
continue
|
|
2841
|
+
if line.startswith("+"):
|
|
2842
|
+
new_lines.append(line[1:])
|
|
2843
|
+
continue
|
|
2844
|
+
raise ValueError("update lines must start with space, -, +, or @@")
|
|
2845
|
+
if old_lines == new_lines:
|
|
2846
|
+
raise ValueError("update file patch must change file content")
|
|
2847
|
+
content_lines = content.splitlines()
|
|
2848
|
+
start = _find_subsequence(content_lines, old_lines)
|
|
2849
|
+
if start is None:
|
|
2850
|
+
raise ValueError(f"update context not found: {relative_path}")
|
|
2851
|
+
next_lines = content_lines[:start] + new_lines + content_lines[start + len(old_lines) :]
|
|
2852
|
+
return "\n".join(next_lines) + "\n"
|
|
2853
|
+
|
|
2854
|
+
|
|
2855
|
+
def _find_subsequence(lines: list[str], needle: list[str]) -> int | None:
|
|
2856
|
+
if not needle:
|
|
2857
|
+
return None
|
|
2858
|
+
last_start = len(lines) - len(needle)
|
|
2859
|
+
for index in range(last_start + 1):
|
|
2860
|
+
if lines[index : index + len(needle)] == needle:
|
|
2861
|
+
return index
|
|
2862
|
+
return None
|
|
2863
|
+
|
|
2864
|
+
|
|
2865
|
+
def _iter_workspace_entries(root: Path, *, max_depth: int):
|
|
2866
|
+
root_depth = len(root.parts)
|
|
2867
|
+
for path in sorted(root.rglob("*"), key=lambda item: item.relative_to(root).as_posix()):
|
|
2868
|
+
if path.is_symlink():
|
|
2869
|
+
continue
|
|
2870
|
+
depth = len(path.parts) - root_depth
|
|
2871
|
+
if depth > max_depth:
|
|
2872
|
+
continue
|
|
2873
|
+
yield path
|
|
2874
|
+
|
|
2875
|
+
|
|
2876
|
+
def _file_entry(workspace_root: Path, path: Path) -> Dict[str, Any]:
|
|
2877
|
+
if path.is_dir():
|
|
2878
|
+
return {
|
|
2879
|
+
"path": _workspace_output_path(workspace_root, path),
|
|
2880
|
+
"type": "directory",
|
|
2881
|
+
"bytes": 0,
|
|
2882
|
+
}
|
|
2883
|
+
return {
|
|
2884
|
+
"path": _workspace_output_path(workspace_root, path),
|
|
2885
|
+
"type": "file",
|
|
2886
|
+
"bytes": path.stat().st_size,
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
|
|
2890
|
+
def _workspace_output_path(workspace_root: Path, path: Path) -> str:
|
|
2891
|
+
try:
|
|
2892
|
+
relative = path.relative_to(workspace_root)
|
|
2893
|
+
except ValueError as exc:
|
|
2894
|
+
raise ValueError("path must stay inside the workspace") from exc
|
|
2895
|
+
output = relative.as_posix()
|
|
2896
|
+
return "." if output == "" else output
|
|
2897
|
+
|
|
2898
|
+
|
|
2899
|
+
def _resolve_workspace_relative_path(workspace_root: Path, relative_path: str) -> Path:
|
|
2900
|
+
candidate_path = Path(relative_path)
|
|
2901
|
+
if candidate_path.is_absolute():
|
|
2902
|
+
raise ValueError("path must stay inside the workspace")
|
|
2903
|
+
if any(part in {"", ".", ".."} for part in candidate_path.parts):
|
|
2904
|
+
raise ValueError("path must stay inside the workspace")
|
|
2905
|
+
_reject_symlink_path_parts(workspace_root, candidate_path)
|
|
2906
|
+
normalized_path = os.path.normpath(relative_path)
|
|
2907
|
+
target = (workspace_root / normalized_path).resolve()
|
|
2908
|
+
try:
|
|
2909
|
+
target.relative_to(workspace_root)
|
|
2910
|
+
except ValueError as exc:
|
|
2911
|
+
raise ValueError("path must stay inside the workspace") from exc
|
|
2912
|
+
return target
|
|
2913
|
+
|
|
2914
|
+
|
|
2915
|
+
def _reject_symlink_path_parts(workspace_root: Path, candidate_path: Path) -> None:
|
|
2916
|
+
current = workspace_root
|
|
2917
|
+
for part in candidate_path.parts:
|
|
2918
|
+
current = current / part
|
|
2919
|
+
if current.is_symlink():
|
|
2920
|
+
raise ValueError("path must not be a symlink")
|
|
2921
|
+
if not current.exists():
|
|
2922
|
+
return
|
|
2923
|
+
|
|
2924
|
+
|
|
2925
|
+
def _http_request(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
2926
|
+
url = input_payload.get("url")
|
|
2927
|
+
if not isinstance(url, str) or not url.strip():
|
|
2928
|
+
raise ValueError("url must be a non-empty string")
|
|
2929
|
+
normalized_url = url.strip()
|
|
2930
|
+
_validate_http_request_url(normalized_url)
|
|
2931
|
+
max_bytes = int(input_payload.get("max_bytes", _HTTP_REQUEST_MAX_BYTES))
|
|
2932
|
+
request = urllib.request.Request(
|
|
2933
|
+
normalized_url,
|
|
2934
|
+
headers={"User-Agent": "kagent/0.1"},
|
|
2935
|
+
method="GET",
|
|
2936
|
+
)
|
|
2937
|
+
try:
|
|
2938
|
+
with _open_http_request_without_redirects(
|
|
2939
|
+
request,
|
|
2940
|
+
timeout_seconds=_HTTP_REQUEST_TIMEOUT_SECONDS,
|
|
2941
|
+
) as response:
|
|
2942
|
+
body = response.read(max_bytes + 1)
|
|
2943
|
+
content_type = response.headers.get("Content-Type", "")
|
|
2944
|
+
status_code = int(response.status)
|
|
2945
|
+
except urllib.error.HTTPError as exc:
|
|
2946
|
+
body = exc.read(max_bytes + 1)
|
|
2947
|
+
content_type = exc.headers.get("Content-Type", "")
|
|
2948
|
+
status_code = int(exc.code)
|
|
2949
|
+
except urllib.error.URLError as exc:
|
|
2950
|
+
raise ValueError("http request failed") from exc
|
|
2951
|
+
truncated = len(body) > max_bytes
|
|
2952
|
+
if truncated:
|
|
2953
|
+
body = body[:max_bytes]
|
|
2954
|
+
return {
|
|
2955
|
+
"url": normalized_url,
|
|
2956
|
+
"status_code": status_code,
|
|
2957
|
+
"content_type": content_type,
|
|
2958
|
+
"body_text": body.decode("utf-8", errors="replace"),
|
|
2959
|
+
"bytes": len(body),
|
|
2960
|
+
"truncated": truncated,
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
|
|
2964
|
+
def _open_url(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
2965
|
+
url = input_payload.get("url")
|
|
2966
|
+
if not isinstance(url, str) or not url.strip():
|
|
2967
|
+
raise ValueError("url must be a non-empty string")
|
|
2968
|
+
normalized_url = url.strip()
|
|
2969
|
+
_validate_open_url(normalized_url)
|
|
2970
|
+
attempts = [
|
|
2971
|
+
(
|
|
2972
|
+
["osascript", "-e", _chrome_open_location_script(normalized_url)],
|
|
2973
|
+
"Google Chrome",
|
|
2974
|
+
"osascript Google Chrome",
|
|
2975
|
+
),
|
|
2976
|
+
(["open", "-a", "Google Chrome", normalized_url], "Google Chrome", "open -a Google Chrome"),
|
|
2977
|
+
(["open", normalized_url], "default", "open"),
|
|
2978
|
+
]
|
|
2979
|
+
last_error: BaseException | None = None
|
|
2980
|
+
for command_args, application, command_label in attempts:
|
|
2981
|
+
try:
|
|
2982
|
+
subprocess.run(
|
|
2983
|
+
command_args,
|
|
2984
|
+
check=True,
|
|
2985
|
+
capture_output=True,
|
|
2986
|
+
text=True,
|
|
2987
|
+
timeout=_OPEN_COMMAND_TIMEOUT_SECONDS,
|
|
2988
|
+
)
|
|
2989
|
+
return {
|
|
2990
|
+
"url": normalized_url,
|
|
2991
|
+
"opened": True,
|
|
2992
|
+
"application": application,
|
|
2993
|
+
"command": command_label,
|
|
2994
|
+
}
|
|
2995
|
+
except subprocess.TimeoutExpired as exc:
|
|
2996
|
+
raise TimeoutError("open url command timed out") from exc
|
|
2997
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
2998
|
+
last_error = exc
|
|
2999
|
+
continue
|
|
3000
|
+
raise ValueError("open url failed") from last_error
|
|
3001
|
+
|
|
3002
|
+
|
|
3003
|
+
def _open_app(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
3004
|
+
application = input_payload.get("application")
|
|
3005
|
+
if not isinstance(application, str) or not application.strip():
|
|
3006
|
+
raise ValueError("application must be a non-empty string")
|
|
3007
|
+
normalized_application = " ".join(application.strip().split())
|
|
3008
|
+
_validate_open_app_name(normalized_application)
|
|
3009
|
+
try:
|
|
3010
|
+
subprocess.run(
|
|
3011
|
+
["open", "-a", normalized_application],
|
|
3012
|
+
check=True,
|
|
3013
|
+
capture_output=True,
|
|
3014
|
+
text=True,
|
|
3015
|
+
timeout=_OPEN_COMMAND_TIMEOUT_SECONDS,
|
|
3016
|
+
)
|
|
3017
|
+
except subprocess.TimeoutExpired as exc:
|
|
3018
|
+
raise TimeoutError("open app command timed out") from exc
|
|
3019
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
3020
|
+
raise ValueError("open app failed") from exc
|
|
3021
|
+
return {
|
|
3022
|
+
"application": normalized_application,
|
|
3023
|
+
"opened": True,
|
|
3024
|
+
"command": "open -a",
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
|
|
3028
|
+
def _validate_open_app_name(application: str) -> None:
|
|
3029
|
+
if len(application) > _APP_NAME_MAX_LENGTH:
|
|
3030
|
+
raise ValueError("application name is too long")
|
|
3031
|
+
if "\x00" in application or "\n" in application or "\r" in application:
|
|
3032
|
+
raise ValueError("application must be a single line")
|
|
3033
|
+
if application.startswith("-"):
|
|
3034
|
+
raise ValueError("application must not start with -")
|
|
3035
|
+
if "/" in application or "\\" in application:
|
|
3036
|
+
raise ValueError("application must be an app name, not a path")
|
|
3037
|
+
if not _APP_NAME_ALLOWED_PATTERN.match(application):
|
|
3038
|
+
raise ValueError("application contains unsupported characters")
|
|
3039
|
+
|
|
3040
|
+
|
|
3041
|
+
def _validate_open_url(url: str) -> None:
|
|
3042
|
+
parsed = urllib.parse.urlparse(url)
|
|
3043
|
+
if parsed.scheme not in {"http", "https"}:
|
|
3044
|
+
raise ValueError("url must start with http:// or https://")
|
|
3045
|
+
if not parsed.hostname:
|
|
3046
|
+
raise ValueError("url host is required")
|
|
3047
|
+
if parsed.username or parsed.password:
|
|
3048
|
+
raise ValueError("url must not contain credentials")
|
|
3049
|
+
_validate_url_without_secret_like_query_or_fragment(parsed)
|
|
3050
|
+
|
|
3051
|
+
|
|
3052
|
+
def _chrome_open_location_script(url: str) -> str:
|
|
3053
|
+
escaped_url = _apple_script_string(url)
|
|
3054
|
+
return (
|
|
3055
|
+
'tell application "Google Chrome"\n'
|
|
3056
|
+
" activate\n"
|
|
3057
|
+
" if (count of windows) = 0 then make new window\n"
|
|
3058
|
+
" tell front window\n"
|
|
3059
|
+
f" make new tab at end of tabs with properties {{URL:{escaped_url}}}\n"
|
|
3060
|
+
" set active tab index to (count of tabs)\n"
|
|
3061
|
+
" end tell\n"
|
|
3062
|
+
"end tell"
|
|
3063
|
+
)
|
|
3064
|
+
|
|
3065
|
+
|
|
3066
|
+
def _apple_script_string(value: str) -> str:
|
|
3067
|
+
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
3068
|
+
|
|
3069
|
+
|
|
3070
|
+
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
3071
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
3072
|
+
return None
|
|
3073
|
+
|
|
3074
|
+
|
|
3075
|
+
def _open_http_request_without_redirects(
|
|
3076
|
+
request: urllib.request.Request,
|
|
3077
|
+
*,
|
|
3078
|
+
timeout_seconds: float,
|
|
3079
|
+
):
|
|
3080
|
+
opener = urllib.request.build_opener(_NoRedirectHandler)
|
|
3081
|
+
try:
|
|
3082
|
+
return opener.open(request, timeout=timeout_seconds)
|
|
3083
|
+
except urllib.error.HTTPError as exc:
|
|
3084
|
+
if exc.code in _HTTP_REDIRECT_STATUS_CODES:
|
|
3085
|
+
return exc
|
|
3086
|
+
raise
|
|
3087
|
+
|
|
3088
|
+
|
|
3089
|
+
def _validate_http_request_url(url: str) -> None:
|
|
3090
|
+
parsed = urllib.parse.urlparse(url)
|
|
3091
|
+
if parsed.scheme not in {"http", "https"}:
|
|
3092
|
+
raise ValueError("url must start with http:// or https://")
|
|
3093
|
+
if not parsed.hostname:
|
|
3094
|
+
raise ValueError("url host is required")
|
|
3095
|
+
if parsed.username or parsed.password:
|
|
3096
|
+
raise ValueError("url must not contain credentials")
|
|
3097
|
+
_validate_url_without_secret_like_query_or_fragment(parsed)
|
|
3098
|
+
host = parsed.hostname.strip().lower()
|
|
3099
|
+
if host in _BLOCKED_HTTP_HOSTS or host.endswith(".localhost"):
|
|
3100
|
+
raise ValueError("url host is not allowed")
|
|
3101
|
+
try:
|
|
3102
|
+
address = ipaddress.ip_address(host)
|
|
3103
|
+
except ValueError:
|
|
3104
|
+
_validate_resolved_http_host(host, parsed.port)
|
|
3105
|
+
return
|
|
3106
|
+
if _is_blocked_http_address(address):
|
|
3107
|
+
raise ValueError("url host is not allowed")
|
|
3108
|
+
|
|
3109
|
+
|
|
3110
|
+
def _validate_resolved_http_host(host: str, port: int | None) -> None:
|
|
3111
|
+
try:
|
|
3112
|
+
records = socket.getaddrinfo(host, port or 443, type=socket.SOCK_STREAM)
|
|
3113
|
+
except socket.gaierror as exc:
|
|
3114
|
+
raise ValueError("url host could not be resolved") from exc
|
|
3115
|
+
for record in records:
|
|
3116
|
+
sockaddr = record[4]
|
|
3117
|
+
if not sockaddr:
|
|
3118
|
+
continue
|
|
3119
|
+
try:
|
|
3120
|
+
address = ipaddress.ip_address(sockaddr[0])
|
|
3121
|
+
except ValueError as exc:
|
|
3122
|
+
raise ValueError("url host resolved to an invalid address") from exc
|
|
3123
|
+
if _is_blocked_http_address(address):
|
|
3124
|
+
raise ValueError("url host is not allowed")
|
|
3125
|
+
|
|
3126
|
+
|
|
3127
|
+
def _validate_url_without_secret_like_query_or_fragment(
|
|
3128
|
+
parsed: urllib.parse.ParseResult,
|
|
3129
|
+
) -> None:
|
|
3130
|
+
for item in (parsed.query, parsed.fragment):
|
|
3131
|
+
if _has_secret_like_url_part(item):
|
|
3132
|
+
raise ValueError("url must not contain secret-like query or fragment")
|
|
3133
|
+
|
|
3134
|
+
|
|
3135
|
+
def _has_secret_like_url_part(value: str) -> bool:
|
|
3136
|
+
if not value:
|
|
3137
|
+
return False
|
|
3138
|
+
pairs = urllib.parse.parse_qsl(value, keep_blank_values=True)
|
|
3139
|
+
if not pairs:
|
|
3140
|
+
pairs = [(value, "")]
|
|
3141
|
+
for key, item in pairs:
|
|
3142
|
+
normalized_key = key.lower().replace("-", "_")
|
|
3143
|
+
if any(part in normalized_key for part in _URL_SECRET_KEY_PARTS):
|
|
3144
|
+
return True
|
|
3145
|
+
if any(pattern.search(item) for pattern in _URL_SECRET_VALUE_PATTERNS):
|
|
3146
|
+
return True
|
|
3147
|
+
return False
|
|
3148
|
+
|
|
3149
|
+
|
|
3150
|
+
def _is_blocked_http_address(address: ipaddress._BaseAddress) -> bool:
|
|
3151
|
+
return (
|
|
3152
|
+
address.is_loopback
|
|
3153
|
+
or address.is_private
|
|
3154
|
+
or address.is_link_local
|
|
3155
|
+
or address.is_multicast
|
|
3156
|
+
or address.is_reserved
|
|
3157
|
+
or address.is_unspecified
|
|
3158
|
+
)
|
|
3159
|
+
|
|
3160
|
+
|
|
3161
|
+
def _artifact(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
3162
|
+
title = input_payload.get("title")
|
|
3163
|
+
kind = input_payload.get("kind")
|
|
3164
|
+
content = input_payload.get("content")
|
|
3165
|
+
artifact_format = input_payload.get("format", "markdown")
|
|
3166
|
+
tags = input_payload.get("tags", [])
|
|
3167
|
+
|
|
3168
|
+
if not isinstance(title, str) or not title.strip():
|
|
3169
|
+
raise ValueError("title must be a non-empty string")
|
|
3170
|
+
if kind not in _ARTIFACT_KINDS:
|
|
3171
|
+
raise ValueError("kind must be report, plan, decision, data, or message")
|
|
3172
|
+
if not isinstance(content, str) or not content.strip():
|
|
3173
|
+
raise ValueError("content must be a non-empty string")
|
|
3174
|
+
if artifact_format not in _ARTIFACT_FORMATS:
|
|
3175
|
+
raise ValueError("format must be markdown, plain_text, or json")
|
|
3176
|
+
if not isinstance(tags, list):
|
|
3177
|
+
raise ValueError("tags must be an array")
|
|
3178
|
+
|
|
3179
|
+
normalized_title = title.strip()
|
|
3180
|
+
normalized_tags = []
|
|
3181
|
+
for index, tag in enumerate(tags):
|
|
3182
|
+
if not isinstance(tag, str):
|
|
3183
|
+
raise ValueError(f"tag {index} must be a string")
|
|
3184
|
+
normalized_tag = tag.strip()
|
|
3185
|
+
if normalized_tag:
|
|
3186
|
+
normalized_tags.append(normalized_tag)
|
|
3187
|
+
|
|
3188
|
+
artifact_id = _artifact_id(normalized_title, str(kind), str(artifact_format), content)
|
|
3189
|
+
return {
|
|
3190
|
+
"artifact_id": artifact_id,
|
|
3191
|
+
"title": normalized_title,
|
|
3192
|
+
"kind": kind,
|
|
3193
|
+
"format": artifact_format,
|
|
3194
|
+
"content": content,
|
|
3195
|
+
"tags": normalized_tags,
|
|
3196
|
+
"bytes": len(content.encode("utf-8")),
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
|
|
3200
|
+
def _artifact_id(title: str, kind: str, artifact_format: str, content: str) -> str:
|
|
3201
|
+
digest = hashlib.sha256(
|
|
3202
|
+
"|".join([title, kind, artifact_format, content]).encode("utf-8")
|
|
3203
|
+
).hexdigest()
|
|
3204
|
+
return f"artifact_{digest[:12]}"
|
|
3205
|
+
|
|
3206
|
+
|
|
3207
|
+
def _decision_matrix(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
3208
|
+
question = input_payload.get("question")
|
|
3209
|
+
criteria = input_payload.get("criteria")
|
|
3210
|
+
options = input_payload.get("options")
|
|
3211
|
+
|
|
3212
|
+
if not isinstance(question, str) or not question.strip():
|
|
3213
|
+
raise ValueError("question must be a non-empty string")
|
|
3214
|
+
if not isinstance(criteria, list) or not criteria:
|
|
3215
|
+
raise ValueError("criteria must be a non-empty array")
|
|
3216
|
+
if not isinstance(options, list) or len(options) < 2:
|
|
3217
|
+
raise ValueError("options must contain at least 2 item(s)")
|
|
3218
|
+
|
|
3219
|
+
normalized_criteria = _normalize_decision_criteria(criteria)
|
|
3220
|
+
rankings = _rank_decision_options(options, normalized_criteria)
|
|
3221
|
+
return {
|
|
3222
|
+
"question": question.strip(),
|
|
3223
|
+
"criteria": normalized_criteria,
|
|
3224
|
+
"rankings": rankings,
|
|
3225
|
+
"winner": rankings[0]["name"],
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
|
|
3229
|
+
def _normalize_decision_criteria(criteria: list[Any]) -> list[Dict[str, Any]]:
|
|
3230
|
+
normalized = []
|
|
3231
|
+
for index, criterion in enumerate(criteria):
|
|
3232
|
+
if not isinstance(criterion, dict):
|
|
3233
|
+
raise ValueError(f"criterion {index} must be an object")
|
|
3234
|
+
name = criterion.get("name")
|
|
3235
|
+
weight = criterion.get("weight")
|
|
3236
|
+
if not isinstance(name, str) or not name.strip():
|
|
3237
|
+
raise ValueError(f"criterion {index} name must be a non-empty string")
|
|
3238
|
+
if not _is_number(weight):
|
|
3239
|
+
raise ValueError(f"criterion {index} weight must be a number")
|
|
3240
|
+
normalized.append({"name": name.strip(), "weight": float(weight)})
|
|
3241
|
+
return normalized
|
|
3242
|
+
|
|
3243
|
+
|
|
3244
|
+
def _rank_decision_options(
|
|
3245
|
+
options: list[Any],
|
|
3246
|
+
criteria: list[Dict[str, Any]],
|
|
3247
|
+
) -> list[Dict[str, Any]]:
|
|
3248
|
+
scored_options = []
|
|
3249
|
+
for index, option in enumerate(options):
|
|
3250
|
+
if not isinstance(option, dict):
|
|
3251
|
+
raise ValueError(f"option {index} must be an object")
|
|
3252
|
+
name = option.get("name")
|
|
3253
|
+
scores = option.get("scores")
|
|
3254
|
+
rationale = option.get("rationale", "")
|
|
3255
|
+
if not isinstance(name, str) or not name.strip():
|
|
3256
|
+
raise ValueError(f"option {index} name must be a non-empty string")
|
|
3257
|
+
if not isinstance(scores, list) or len(scores) != len(criteria):
|
|
3258
|
+
raise ValueError(
|
|
3259
|
+
f"option {index} scores must match the number of criteria"
|
|
3260
|
+
)
|
|
3261
|
+
if not isinstance(rationale, str):
|
|
3262
|
+
raise ValueError(f"option {index} rationale must be a string")
|
|
3263
|
+
normalized_scores = []
|
|
3264
|
+
total = 0.0
|
|
3265
|
+
for score_index, score in enumerate(scores):
|
|
3266
|
+
if not _is_number(score):
|
|
3267
|
+
raise ValueError(f"option {index} score {score_index} must be a number")
|
|
3268
|
+
normalized_score = float(score)
|
|
3269
|
+
normalized_scores.append(normalized_score)
|
|
3270
|
+
total += normalized_score * float(criteria[score_index]["weight"])
|
|
3271
|
+
scored_options.append(
|
|
3272
|
+
{
|
|
3273
|
+
"name": name.strip(),
|
|
3274
|
+
"score": round(total, 4),
|
|
3275
|
+
"scores": normalized_scores,
|
|
3276
|
+
"rationale": rationale.strip(),
|
|
3277
|
+
}
|
|
3278
|
+
)
|
|
3279
|
+
|
|
3280
|
+
ranked = sorted(scored_options, key=lambda item: (-item["score"], item["name"]))
|
|
3281
|
+
for index, option in enumerate(ranked, start=1):
|
|
3282
|
+
option["rank"] = index
|
|
3283
|
+
return [
|
|
3284
|
+
{
|
|
3285
|
+
"rank": option["rank"],
|
|
3286
|
+
"name": option["name"],
|
|
3287
|
+
"score": option["score"],
|
|
3288
|
+
"scores": option["scores"],
|
|
3289
|
+
"rationale": option["rationale"],
|
|
3290
|
+
}
|
|
3291
|
+
for option in ranked
|
|
3292
|
+
]
|
|
3293
|
+
|
|
3294
|
+
|
|
3295
|
+
def _validate_tool_input(value: Any, schema: Dict[str, Any], path: str = "input") -> None:
|
|
3296
|
+
expected_type = schema.get("type")
|
|
3297
|
+
if expected_type == "object":
|
|
3298
|
+
_validate_object(value, schema, path)
|
|
3299
|
+
elif expected_type == "array":
|
|
3300
|
+
_validate_array(value, schema, path)
|
|
3301
|
+
elif expected_type == "string":
|
|
3302
|
+
if not isinstance(value, str):
|
|
3303
|
+
raise ValueError(f"{path} must be a string")
|
|
3304
|
+
min_length = schema.get("minLength")
|
|
3305
|
+
if isinstance(min_length, int) and len(value.strip()) < min_length:
|
|
3306
|
+
raise ValueError(
|
|
3307
|
+
f"{path} must contain at least {min_length} character(s)"
|
|
3308
|
+
)
|
|
3309
|
+
max_length = schema.get("maxLength")
|
|
3310
|
+
if isinstance(max_length, int) and len(value) > max_length:
|
|
3311
|
+
raise ValueError(f"{path} must contain at most {max_length} character(s)")
|
|
3312
|
+
elif expected_type == "number":
|
|
3313
|
+
if not _is_number(value):
|
|
3314
|
+
raise ValueError(f"{path} must be a number")
|
|
3315
|
+
minimum = schema.get("minimum")
|
|
3316
|
+
if _is_number(minimum) and value < minimum:
|
|
3317
|
+
raise ValueError(f"{path} must be at least {minimum:g}")
|
|
3318
|
+
maximum = schema.get("maximum")
|
|
3319
|
+
if _is_number(maximum) and value > maximum:
|
|
3320
|
+
raise ValueError(f"{path} must be at most {maximum:g}")
|
|
3321
|
+
elif expected_type == "boolean":
|
|
3322
|
+
if not isinstance(value, bool):
|
|
3323
|
+
raise ValueError(f"{path} must be a boolean")
|
|
3324
|
+
|
|
3325
|
+
enum_values = schema.get("enum")
|
|
3326
|
+
if isinstance(enum_values, list) and value not in enum_values:
|
|
3327
|
+
allowed = ", ".join(str(item) for item in enum_values)
|
|
3328
|
+
raise ValueError(f"{path} must be one of: {allowed}")
|
|
3329
|
+
|
|
3330
|
+
|
|
3331
|
+
def _validate_object(value: Any, schema: Dict[str, Any], path: str) -> None:
|
|
3332
|
+
if not isinstance(value, dict):
|
|
3333
|
+
raise ValueError(f"{path} must be an object")
|
|
3334
|
+
properties = schema.get("properties", {})
|
|
3335
|
+
if not isinstance(properties, dict):
|
|
3336
|
+
properties = {}
|
|
3337
|
+
required = schema.get("required", [])
|
|
3338
|
+
if not isinstance(required, list):
|
|
3339
|
+
required = []
|
|
3340
|
+
for key in required:
|
|
3341
|
+
if key not in value:
|
|
3342
|
+
raise ValueError(f"{path}.{key} is required")
|
|
3343
|
+
if schema.get("additionalProperties") is False:
|
|
3344
|
+
for key in value:
|
|
3345
|
+
if key not in properties:
|
|
3346
|
+
raise ValueError(f"{path}.{key} is not allowed")
|
|
3347
|
+
for key, property_schema in properties.items():
|
|
3348
|
+
if key in value and isinstance(property_schema, dict):
|
|
3349
|
+
_validate_tool_input(value[key], property_schema, f"{path}.{key}")
|
|
3350
|
+
|
|
3351
|
+
|
|
3352
|
+
def _validate_array(value: Any, schema: Dict[str, Any], path: str) -> None:
|
|
3353
|
+
if not isinstance(value, list):
|
|
3354
|
+
raise ValueError(f"{path} must be an array")
|
|
3355
|
+
min_items = schema.get("minItems")
|
|
3356
|
+
if isinstance(min_items, int) and len(value) < min_items:
|
|
3357
|
+
raise ValueError(f"{path} must contain at least {min_items} item(s)")
|
|
3358
|
+
max_items = schema.get("maxItems")
|
|
3359
|
+
if isinstance(max_items, int) and len(value) > max_items:
|
|
3360
|
+
raise ValueError(f"{path} must contain at most {max_items} item(s)")
|
|
3361
|
+
item_schema = schema.get("items")
|
|
3362
|
+
if isinstance(item_schema, dict):
|
|
3363
|
+
for index, item in enumerate(value):
|
|
3364
|
+
_validate_tool_input(item, item_schema, f"{path}[{index}]")
|
|
3365
|
+
|
|
3366
|
+
|
|
3367
|
+
def _is_number(value: Any) -> bool:
|
|
3368
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
3369
|
+
|
|
3370
|
+
|
|
3371
|
+
def _utc_timestamp() -> str:
|
|
3372
|
+
return datetime.now(timezone.utc).isoformat()
|
|
3373
|
+
|
|
3374
|
+
|
|
3375
|
+
def _duration_since(started_at: float) -> str:
|
|
3376
|
+
return f"{time.perf_counter() - started_at:.4f}"
|
|
3377
|
+
|
|
3378
|
+
|
|
3379
|
+
def _delegate_task(
|
|
3380
|
+
input_payload: Dict[str, Any],
|
|
3381
|
+
*,
|
|
3382
|
+
delegate_runner: Callable[[str, int], Dict[str, Any]] | None,
|
|
3383
|
+
) -> Dict[str, Any]:
|
|
3384
|
+
if delegate_runner is None:
|
|
3385
|
+
raise ValueError("delegate runner is not configured")
|
|
3386
|
+
goal = input_payload.get("goal")
|
|
3387
|
+
if not isinstance(goal, str) or not goal.strip():
|
|
3388
|
+
raise ValueError("goal must be a non-empty string")
|
|
3389
|
+
max_iterations = int(input_payload.get("max_iterations", 1))
|
|
3390
|
+
if max_iterations < 1 or max_iterations > _DELEGATE_MAX_ITERATIONS:
|
|
3391
|
+
raise ValueError(
|
|
3392
|
+
f"max_iterations must be between 1 and {_DELEGATE_MAX_ITERATIONS}"
|
|
3393
|
+
)
|
|
3394
|
+
child = delegate_runner(goal.strip(), max_iterations)
|
|
3395
|
+
observations = child.get("observations", [])
|
|
3396
|
+
if not isinstance(observations, list):
|
|
3397
|
+
observations = []
|
|
3398
|
+
return {
|
|
3399
|
+
"child_run_id": str(child.get("run_id", "")),
|
|
3400
|
+
"status": str(child.get("status", "")),
|
|
3401
|
+
"answer": str(child.get("answer", "")),
|
|
3402
|
+
"error_code": str(child.get("error_code", "")),
|
|
3403
|
+
"child_iteration_count": str(child.get("iteration_count", "")),
|
|
3404
|
+
"child_observation_count": str(len(observations)),
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
|
|
3408
|
+
def _transform_text(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
3409
|
+
text = input_payload.get("text")
|
|
3410
|
+
mode = input_payload.get("mode")
|
|
3411
|
+
if not isinstance(text, str):
|
|
3412
|
+
raise ValueError("text must be a string")
|
|
3413
|
+
if mode == "uppercase":
|
|
3414
|
+
return {"text": text.upper()}
|
|
3415
|
+
if mode == "lowercase":
|
|
3416
|
+
return {"text": text.lower()}
|
|
3417
|
+
if mode == "reverse":
|
|
3418
|
+
return {"text": text[::-1]}
|
|
3419
|
+
if mode == "trim":
|
|
3420
|
+
return {"text": text.strip()}
|
|
3421
|
+
raise ValueError("mode must be uppercase, lowercase, reverse, or trim")
|
|
3422
|
+
|
|
3423
|
+
|
|
3424
|
+
def _rubric_score(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
3425
|
+
criteria = input_payload.get("criteria")
|
|
3426
|
+
if not isinstance(criteria, list) or not criteria:
|
|
3427
|
+
raise ValueError("criteria must be a non-empty array")
|
|
3428
|
+
|
|
3429
|
+
normalized_criteria = []
|
|
3430
|
+
failed_criteria = []
|
|
3431
|
+
blocking_failures = []
|
|
3432
|
+
passed_count = 0
|
|
3433
|
+
for index, criterion in enumerate(criteria):
|
|
3434
|
+
if not isinstance(criterion, dict):
|
|
3435
|
+
raise ValueError(f"criterion {index} must be an object")
|
|
3436
|
+
name = criterion.get("name")
|
|
3437
|
+
passed = criterion.get("passed")
|
|
3438
|
+
severity = criterion.get("severity", "normal")
|
|
3439
|
+
evidence = criterion.get("evidence", "")
|
|
3440
|
+
if not isinstance(name, str) or not name.strip():
|
|
3441
|
+
raise ValueError(f"criterion {index} name must be a non-empty string")
|
|
3442
|
+
if not isinstance(passed, bool):
|
|
3443
|
+
raise ValueError(f"criterion {index} passed must be a boolean")
|
|
3444
|
+
if severity not in _RUBRIC_SEVERITIES:
|
|
3445
|
+
raise ValueError(f"criterion {index} severity must be low, normal, or blocking")
|
|
3446
|
+
if not isinstance(evidence, str):
|
|
3447
|
+
raise ValueError(f"criterion {index} evidence must be a string")
|
|
3448
|
+
|
|
3449
|
+
normalized_name = name.strip()
|
|
3450
|
+
normalized = {
|
|
3451
|
+
"name": normalized_name,
|
|
3452
|
+
"passed": passed,
|
|
3453
|
+
"severity": severity,
|
|
3454
|
+
"evidence": evidence.strip(),
|
|
3455
|
+
}
|
|
3456
|
+
normalized_criteria.append(normalized)
|
|
3457
|
+
if passed:
|
|
3458
|
+
passed_count += 1
|
|
3459
|
+
continue
|
|
3460
|
+
failed_criteria.append(normalized_name)
|
|
3461
|
+
if severity == "blocking":
|
|
3462
|
+
blocking_failures.append(normalized_name)
|
|
3463
|
+
|
|
3464
|
+
total = len(normalized_criteria)
|
|
3465
|
+
failed_count = total - passed_count
|
|
3466
|
+
return {
|
|
3467
|
+
"criteria": normalized_criteria,
|
|
3468
|
+
"passed": passed_count,
|
|
3469
|
+
"failed": failed_count,
|
|
3470
|
+
"total": total,
|
|
3471
|
+
"score_percent": round((passed_count / total) * 100, 2),
|
|
3472
|
+
"blocking_failures": blocking_failures,
|
|
3473
|
+
"failed_criteria": failed_criteria,
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
|
|
3477
|
+
def _task_list(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
3478
|
+
items = input_payload.get("items")
|
|
3479
|
+
if not isinstance(items, list) or not items:
|
|
3480
|
+
raise ValueError("items must be a non-empty array")
|
|
3481
|
+
|
|
3482
|
+
normalized_items = []
|
|
3483
|
+
counts = {status: 0 for status in _TASK_STATUSES}
|
|
3484
|
+
for index, item in enumerate(items):
|
|
3485
|
+
if not isinstance(item, dict):
|
|
3486
|
+
raise ValueError(f"item {index} must be an object")
|
|
3487
|
+
normalized = _normalize_task_item(item, index)
|
|
3488
|
+
normalized_items.append(normalized)
|
|
3489
|
+
counts[normalized["status"]] += 1
|
|
3490
|
+
return {
|
|
3491
|
+
"items": normalized_items,
|
|
3492
|
+
"counts": counts,
|
|
3493
|
+
"total": len(normalized_items),
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
|
|
3497
|
+
def _task_transition(input_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
3498
|
+
state = input_payload.get("state")
|
|
3499
|
+
event = input_payload.get("event")
|
|
3500
|
+
if not isinstance(state, str):
|
|
3501
|
+
raise ValueError("state must be a string")
|
|
3502
|
+
if not isinstance(event, str):
|
|
3503
|
+
raise ValueError("event must be a string")
|
|
3504
|
+
return TaskStateMachine().transition(state, event)
|
|
3505
|
+
|
|
3506
|
+
|
|
3507
|
+
def _normalize_task_item(item: Dict[str, Any], index: int) -> Dict[str, Any]:
|
|
3508
|
+
title = item.get("title")
|
|
3509
|
+
if not isinstance(title, str) or not title.strip():
|
|
3510
|
+
raise ValueError(f"item {index} title must be a non-empty string")
|
|
3511
|
+
status = item.get("status", "pending")
|
|
3512
|
+
if status not in _TASK_STATUSES:
|
|
3513
|
+
raise ValueError(
|
|
3514
|
+
f"item {index} status must be pending, in_progress, blocked, done, or failed"
|
|
3515
|
+
)
|
|
3516
|
+
priority = item.get("priority", "normal")
|
|
3517
|
+
if priority not in _TASK_PRIORITIES:
|
|
3518
|
+
raise ValueError(f"item {index} priority must be low, normal, or high")
|
|
3519
|
+
|
|
3520
|
+
normalized = {
|
|
3521
|
+
"title": title.strip(),
|
|
3522
|
+
"status": status,
|
|
3523
|
+
"priority": priority,
|
|
3524
|
+
}
|
|
3525
|
+
for optional_field in ["owner", "due"]:
|
|
3526
|
+
value = item.get(optional_field, "")
|
|
3527
|
+
if value == "":
|
|
3528
|
+
continue
|
|
3529
|
+
if not isinstance(value, str):
|
|
3530
|
+
raise ValueError(f"item {index} {optional_field} must be a string")
|
|
3531
|
+
normalized[optional_field] = value
|
|
3532
|
+
return normalized
|