Add formatted tool output fences
This commit is contained in:
parent
2d621d26b9
commit
d3860304af
|
|
@ -225,10 +225,11 @@ func (ag *Agent) Interrupt(cause error) bool {
|
|||
// Event is a typed output event emitted during an agent turn or in response
|
||||
// to a command.
|
||||
type Event struct {
|
||||
Role string
|
||||
Name string
|
||||
Content string
|
||||
ResponseID string
|
||||
Role string
|
||||
Name string
|
||||
Content string
|
||||
ResponseID string
|
||||
OutputFormat string
|
||||
}
|
||||
|
||||
// EventHandler receives events from the agent.
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
|
|||
classify = pc.IsParallelRead
|
||||
}
|
||||
|
||||
var outputFormat func(string) string
|
||||
if oc, ok := srv.(toolsrv.OutputFormatClassifier); ok {
|
||||
outputFormat = oc.OutputFormat
|
||||
}
|
||||
|
||||
var tierFn func(string, json.RawMessage) ResultTier
|
||||
if tc, ok := srv.(toolsrv.TierClassifier); ok {
|
||||
tierFn = func(name string, args json.RawMessage) ResultTier {
|
||||
|
|
@ -144,6 +149,7 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
|
|||
Tools: allTools,
|
||||
Exec: exec,
|
||||
ClassifyTool: classify,
|
||||
OutputFormat: outputFormat,
|
||||
ClassifyTier: tierFn,
|
||||
Hooks: hooks,
|
||||
Preamble: preamble,
|
||||
|
|
|
|||
|
|
@ -43,11 +43,19 @@ const planReinjectInterval = 10
|
|||
|
||||
type toolExecutor func(ctx context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error)
|
||||
|
||||
func toolOutputFormat(cfg agentConfig, name string) string {
|
||||
if cfg.OutputFormat == nil {
|
||||
return ""
|
||||
}
|
||||
return cfg.OutputFormat(name)
|
||||
}
|
||||
|
||||
type agentConfig struct {
|
||||
Backend backend.Backend
|
||||
Tools []backend.Tool
|
||||
Exec toolExecutor
|
||||
ClassifyTool func(name string) bool // nil=treat all as serial; true=parallel-read-safe
|
||||
OutputFormat func(name string) string // empty means plaintext fence
|
||||
ClassifyTier func(name string, args json.RawMessage) ResultTier // nil=TierHot for all; tools self-classify retention
|
||||
Output EventHandler
|
||||
preamble string // compiled system+agent prompt sent as the system role
|
||||
|
|
@ -277,7 +285,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
}
|
||||
if ctx.Err() != nil {
|
||||
cr := cancelledResult(tc)
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: cr.Content})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: cr.Content, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
return cr, true
|
||||
}
|
||||
emit(cfg, Event{Role: "call", Name: tc.Name, Content: string(tc.Arguments)})
|
||||
|
|
@ -291,7 +299,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
if blocked == "" {
|
||||
blocked = fmt.Sprintf("tool %q blocked by hook", tc.Name)
|
||||
}
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: blocked})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: blocked, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
return toolResult{ToolCallID: tc.ID, Name: tc.Name, Content: blocked, IsError: true}, false
|
||||
}
|
||||
}
|
||||
|
|
@ -300,7 +308,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
key := tc.Name + "\x00" + string(tc.Arguments)
|
||||
if v, ok := resultCache.Load(key); ok {
|
||||
cached := v.(string)
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: cached})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: cached, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
return toolResult{ToolCallID: tc.ID, Name: tc.Name, Content: cached}, false
|
||||
}
|
||||
}
|
||||
|
|
@ -315,7 +323,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
return // already at ceiling, drop further chunks
|
||||
}
|
||||
if !streamed {
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
streamed = true
|
||||
}
|
||||
streamBytes += len(data)
|
||||
|
|
@ -324,7 +332,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
excess := streamBytes - defaultToolResultMaxBytes
|
||||
data = data[:len(data)-excess]
|
||||
}
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: data})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: data, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
})
|
||||
out, blocks, err := cfg.Exec(streamCtx, tc.Name, tc.Arguments)
|
||||
if err != nil {
|
||||
|
|
@ -336,7 +344,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
result += "\n\n<system-user-interruption>\n" + injected + "\n</system-user-interruption>"
|
||||
}
|
||||
}
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
return toolResult{ToolCallID: tc.ID, Name: tc.Name, Content: result, IsError: true}, true
|
||||
}
|
||||
var rlErr *toolsrv.RateLimitedError
|
||||
|
|
@ -389,11 +397,11 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
*resultCache = sync.Map{}
|
||||
}
|
||||
if !streamed {
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
} else if suffix != "" {
|
||||
// Emit suffixes that were appended after streaming completed
|
||||
// so they appear at the end of the chat output.
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: suffix})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: suffix, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
}
|
||||
tier := TierHot
|
||||
if !isErr && cfg.ClassifyTier != nil {
|
||||
|
|
@ -410,7 +418,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
if ctx.Err() != nil {
|
||||
for _, remaining := range toolCalls[i:] {
|
||||
cr := cancelledResult(remaining)
|
||||
emit(cfg, Event{Role: "tool", Name: remaining.Name, Content: cr.Content})
|
||||
emit(cfg, Event{Role: "tool", Name: remaining.Name, Content: cr.Content, OutputFormat: toolOutputFormat(cfg, remaining.Name)})
|
||||
results = append(results, cr)
|
||||
}
|
||||
interrupted = true
|
||||
|
|
@ -429,7 +437,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
fillCancelled := func(from int) {
|
||||
for _, remaining := range toolCalls[from:] {
|
||||
cr := cancelledResult(remaining)
|
||||
emit(cfg, Event{Role: "tool", Name: remaining.Name, Content: cr.Content})
|
||||
emit(cfg, Event{Role: "tool", Name: remaining.Name, Content: cr.Content, OutputFormat: toolOutputFormat(cfg, remaining.Name)})
|
||||
results = append(results, cr)
|
||||
}
|
||||
}
|
||||
|
|
@ -485,7 +493,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
|||
}
|
||||
batchInt[k] = uniqueResults[first].wasInt
|
||||
emit(cfg, Event{Role: "call", Name: tc.Name, Content: string(tc.Arguments)})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: batchResults[k].Content})
|
||||
emit(cfg, Event{Role: "tool", Name: tc.Name, Content: batchResults[k].Content, OutputFormat: toolOutputFormat(cfg, tc.Name)})
|
||||
}
|
||||
}
|
||||
// Add all batch results — model needs a result for every tool call.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type Runtime struct {
|
|||
Tools []backend.Tool
|
||||
Exec toolExecutor
|
||||
ClassifyTool toolClassifier
|
||||
OutputFormat func(string) string
|
||||
ClassifyTier func(string, json.RawMessage) ResultTier
|
||||
GenParams backend.GenerationParams
|
||||
MaxSteps int
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
|
|||
Tools: tools,
|
||||
Exec: ag.runtime.Exec,
|
||||
ClassifyTool: ag.runtime.ClassifyTool,
|
||||
OutputFormat: ag.runtime.OutputFormat,
|
||||
ClassifyTier: ag.runtime.ClassifyTier,
|
||||
GenerationParams: ag.runtime.GenParams,
|
||||
MaxSteps: ag.runtime.MaxSteps,
|
||||
|
|
|
|||
|
|
@ -25,10 +25,7 @@ from _lib.args import parse_args
|
|||
def format_diff(path, old, new):
|
||||
old_lines = old.splitlines(keepends=True)
|
||||
new_lines = new.splitlines(keepends=True)
|
||||
diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||
if not diff:
|
||||
return None
|
||||
return f"```diff\n{diff}```"
|
||||
return ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||
|
||||
def find_exact(content, find):
|
||||
return find if find in content else None
|
||||
|
|
@ -137,9 +134,9 @@ except OSError as e:
|
|||
sys.exit(1)
|
||||
|
||||
diff = format_diff(file_path, original, new_content)
|
||||
if diff is None:
|
||||
if not diff:
|
||||
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
|
||||
print("(no changes)")
|
||||
else:
|
||||
print("STATUS=done STATE_CHANGED=true")
|
||||
print(diff)
|
||||
print(diff, end="" if diff.endswith("\n") else "\n")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"description": "Replace text in a file. Has fuzzy matching (whitespace/indent flexible).",
|
||||
"outputFormat": "diff",
|
||||
"prompt": "## file_edit\n\nReplace text in a file. Has fuzzy matching (whitespace/indent flexible).\n\n**Args**: `path`, `old_string`, `new_string` (required); `replace_all` (optional)\n\n```\nfile_edit(path=\"/abs/path\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"/abs/path\", old_string=\"old text\", new_string=\"new text\", replace_all=\"true\")\n```\n\n**Constraints**: Absolute paths only. Errors if `old_string` matches multiple locations — add surrounding context to disambiguate, or use `replace_all=true`.",
|
||||
"args": {
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -24,12 +24,7 @@ from _lib.args import parse_args
|
|||
def format_diff(path, old, new):
|
||||
old_lines = old.splitlines(keepends=True)
|
||||
new_lines = new.splitlines(keepends=True)
|
||||
diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||
if not diff:
|
||||
return None
|
||||
if not diff.endswith('\n'):
|
||||
diff += '\n'
|
||||
return f"```diff\n{diff}```"
|
||||
return ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||
|
||||
args = parse_args()
|
||||
file_path = args.require("path")
|
||||
|
|
@ -64,9 +59,9 @@ except OSError as e:
|
|||
sys.exit(1)
|
||||
|
||||
diff = format_diff(file_path, old_content, content)
|
||||
if diff is None:
|
||||
if not diff:
|
||||
print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false")
|
||||
print("(no changes)")
|
||||
else:
|
||||
print("STATUS=done STATE_CHANGED=true")
|
||||
print(diff)
|
||||
print(diff, end="" if diff.endswith("\n") else "\n")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"description": "Create or overwrite a file. Produces a unified diff.",
|
||||
"outputFormat": "diff",
|
||||
"prompt": "## file_write\n\nCreate or overwrite a file. Produces a unified diff.\n\n**Args**: `[path, content]`\n\n```\nfile_write(path=\"/abs/path\", content=\"file content here\")\n```\n\n**Constraints**: Absolute paths only. Parent directory must exist.",
|
||||
"args": {
|
||||
"type": "object",
|
||||
|
|
|
|||
11
fs/format.go
11
fs/format.go
|
|
@ -34,9 +34,14 @@ func FormatEvent(ev agent.Event) []byte {
|
|||
return []byte(ev.Content)
|
||||
case "call":
|
||||
args := squashWhitespace(ev.Content)
|
||||
return []byte("[[[call:" + ev.Name + "]]]\n" + args + "\n[[[end]]]\n")
|
||||
return []byte("[[[call:" + ev.Name + "]]]\n```\n" + args + "\n```\n[[[end]]]\n")
|
||||
case "tool":
|
||||
return []byte("[[[tool:" + ev.Name + "]]]\n" + strings.TrimRight(ev.Content, "\n") + "\n[[[end]]]\n")
|
||||
format := ev.OutputFormat
|
||||
fence := "```"
|
||||
if format != "" {
|
||||
fence += format
|
||||
}
|
||||
return []byte("[[[tool:" + ev.Name + "]]]\n" + fence + "\n" + strings.TrimRight(ev.Content, "\n") + "\n```\n[[[end]]]\n")
|
||||
case "error":
|
||||
return []byte("[[[error]]]\n" + ev.Content + "\n[[[end]]]\n")
|
||||
case "info":
|
||||
|
|
@ -89,7 +94,7 @@ func replayMessagesToLog(as *AgentLog, messages []backend.Message) {
|
|||
}
|
||||
as.AppendLog([]byte("[[[end]]]\n"))
|
||||
case "tool":
|
||||
as.AppendLog([]byte("[[[tool]]]\n" + strings.TrimRight(m.Content, "\n") + "\n[[[end]]]\n"))
|
||||
as.AppendLog([]byte("[[[tool]]]\n```\n" + strings.TrimRight(m.Content, "\n") + "\n```\n[[[end]]]\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
fs/types.go
16
fs/types.go
|
|
@ -274,12 +274,17 @@ func (as *AgentLog) Remote() string { return as.remote }
|
|||
func NewEventHandler(as *AgentLog) agent.EventHandler {
|
||||
streamingRole := ""
|
||||
streamingResponseID := ""
|
||||
streamingFence := ""
|
||||
|
||||
return func(ev agent.Event) {
|
||||
switch ev.Role {
|
||||
case "assistant":
|
||||
if streamingRole != ev.Role || (ev.Role == "assistant" && ev.ResponseID != streamingResponseID) {
|
||||
if streamingRole != "" {
|
||||
if streamingFence != "" {
|
||||
as.AppendLog([]byte("\n" + streamingFence + "\n"))
|
||||
streamingFence = ""
|
||||
}
|
||||
as.AppendLog([]byte("\n[[[end]]]\n"))
|
||||
}
|
||||
if ev.Role == "assistant" && ev.ResponseID != "" {
|
||||
|
|
@ -318,6 +323,10 @@ func NewEventHandler(as *AgentLog) agent.EventHandler {
|
|||
}
|
||||
if streamingRole == "tool" {
|
||||
if ev.Content == "" {
|
||||
if streamingFence != "" {
|
||||
as.AppendLog([]byte("\n" + streamingFence + "\n"))
|
||||
streamingFence = ""
|
||||
}
|
||||
as.AppendLog([]byte("\n"))
|
||||
streamingRole = ""
|
||||
} else {
|
||||
|
|
@ -326,11 +335,16 @@ func NewEventHandler(as *AgentLog) agent.EventHandler {
|
|||
return
|
||||
}
|
||||
if streamingRole != "" {
|
||||
if streamingFence != "" {
|
||||
as.AppendLog([]byte("\n" + streamingFence + "\n"))
|
||||
streamingFence = ""
|
||||
}
|
||||
as.AppendLog([]byte("\n[[[end]]]\n"))
|
||||
streamingRole = ""
|
||||
}
|
||||
if ev.Content == "" {
|
||||
as.AppendLog([]byte("[[[tool:" + ev.Name + "]]]\n"))
|
||||
as.AppendLog([]byte("[[[tool:" + ev.Name + "]]]\n```" + ev.OutputFormat + "\n"))
|
||||
streamingFence = "```"
|
||||
streamingRole = "tool"
|
||||
} else {
|
||||
as.AppendLog(FormatEvent(ev))
|
||||
|
|
|
|||
2
kde
2
kde
|
|
@ -1 +1 @@
|
|||
Subproject commit 220adf9b0566cdb66529f5a425894b9e5b63bd7f
|
||||
Subproject commit c983e2bb6af6769f9dd4b757bf449abbf2f2b066
|
||||
|
|
@ -11,28 +11,30 @@ import (
|
|||
|
||||
// MetaFile is the JSON structure of a .meta sidecar file.
|
||||
type MetaFile struct {
|
||||
Description string `json:"description"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Args json.RawMessage `json:"args,omitempty"`
|
||||
Tier string `json:"tier,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
Cmd string `json:"cmd,omitempty"`
|
||||
Sudo bool `json:"sudo,omitempty"`
|
||||
Variants []Variant `json:"variants,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Args json.RawMessage `json:"args,omitempty"`
|
||||
Tier string `json:"tier,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
OutputFormat string `json:"outputFormat,omitempty"`
|
||||
Cmd string `json:"cmd,omitempty"`
|
||||
Sudo bool `json:"sudo,omitempty"`
|
||||
Variants []Variant `json:"variants,omitempty"`
|
||||
}
|
||||
|
||||
// Variant is a conditional tool definition. The first variant whose
|
||||
// match conditions are all satisfied determines the tool's schema,
|
||||
// documentation, and executable.
|
||||
type Variant struct {
|
||||
Match map[string]any `json:"match"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Args json.RawMessage `json:"args,omitempty"`
|
||||
Tier string `json:"tier,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
Cmd string `json:"cmd,omitempty"`
|
||||
Sudo bool `json:"sudo,omitempty"`
|
||||
Match map[string]any `json:"match"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Args json.RawMessage `json:"args,omitempty"`
|
||||
Tier string `json:"tier,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
OutputFormat string `json:"outputFormat,omitempty"`
|
||||
Cmd string `json:"cmd,omitempty"`
|
||||
Sudo bool `json:"sudo,omitempty"`
|
||||
}
|
||||
|
||||
// LoadMetaFile reads and parses a .meta sidecar file for the given tool name.
|
||||
|
|
@ -77,6 +79,9 @@ func (m *MetaFile) Resolve() *MetaFile {
|
|||
if v.Tier != "" {
|
||||
resolved.Tier = v.Tier
|
||||
}
|
||||
if v.OutputFormat != "" {
|
||||
resolved.OutputFormat = v.OutputFormat
|
||||
}
|
||||
if v.ReadOnly {
|
||||
resolved.ReadOnly = true
|
||||
}
|
||||
|
|
@ -171,12 +176,13 @@ func ToolInfoFromMeta(name string, m *MetaFile) ToolInfo {
|
|||
tier = "hot"
|
||||
}
|
||||
return ToolInfo{
|
||||
Name: name,
|
||||
Description: resolved.Description,
|
||||
InputSchema: args,
|
||||
Prompt: resolved.Prompt,
|
||||
Tier: tier,
|
||||
ReadOnly: resolved.ReadOnly,
|
||||
Name: name,
|
||||
Description: resolved.Description,
|
||||
InputSchema: args,
|
||||
Prompt: resolved.Prompt,
|
||||
Tier: tier,
|
||||
ReadOnly: resolved.ReadOnly,
|
||||
OutputFormat: resolved.OutputFormat,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,24 +70,11 @@ func TestSocketSpawnAndDial(t *testing.T) {
|
|||
// SetCWD.
|
||||
conn.SetCWD("/tmp")
|
||||
|
||||
// ListTools.
|
||||
tools, err := conn.ListTools()
|
||||
if err != nil {
|
||||
// ListTools. The remote server starts with no promoted tools; tools are
|
||||
// loaded dynamically through the session tool registry.
|
||||
if _, err := conn.ListTools(); err != nil {
|
||||
t.Fatalf("ListTools: %v", err)
|
||||
}
|
||||
if len(tools) == 0 {
|
||||
t.Error("expected at least one tool (shell)")
|
||||
}
|
||||
found := false
|
||||
for _, ti := range tools {
|
||||
if ti.Name == "shell" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected 'shell' in tool list")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSocketMultipleClients verifies multiple clients can connect simultaneously.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,23 @@ func (e *Server) ResultTier(name string) string {
|
|||
return "hot"
|
||||
}
|
||||
|
||||
// OutputFormat returns the source-fence language for a tool's output.
|
||||
// Empty means the output should be wrapped in a plaintext fence.
|
||||
func (e *Server) OutputFormat(name string) string {
|
||||
if e.toolRegistry != nil && e.sessionID != "" {
|
||||
if info, ok := e.toolRegistry.Lookup(e.sessionID, name); ok {
|
||||
return info.OutputFormat
|
||||
}
|
||||
}
|
||||
m, err := LoadMetaFile(name)
|
||||
if err == nil && m != nil {
|
||||
if resolved := m.Resolve(); resolved != nil {
|
||||
return resolved.OutputFormat
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResultTierArgs classifies the tier using both the outer tool name and its
|
||||
// arguments. For shell, it returns warm. For promoted tools, it
|
||||
// delegates to ResultTier.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ type ToolInfo struct {
|
|||
Tier string
|
||||
// ReadOnly is true when the tool carries an "ollie:parallel read" annotation.
|
||||
ReadOnly bool
|
||||
// OutputFormat is the source-fence language for tool output. Empty means plaintext.
|
||||
OutputFormat string
|
||||
}
|
||||
|
||||
// Runner is the minimal interface satisfied by any tool server (local or remote).
|
||||
|
|
@ -47,5 +49,10 @@ type TierClassifier interface {
|
|||
ResultTierArgs(name string, args json.RawMessage) string
|
||||
}
|
||||
|
||||
// OutputFormatClassifier reports the source-fence language for tool output.
|
||||
type OutputFormatClassifier interface {
|
||||
OutputFormat(name string) string
|
||||
}
|
||||
|
||||
// Handler is the signature for built-in tool handlers.
|
||||
type Handler func(ctx context.Context, srv *Server, args json.RawMessage) (string, error)
|
||||
|
|
|
|||
Loading…
Reference in New Issue