Skip to content

API: Add historyContext to TaskTemplate for injecting prior task outcomes into agent promptsΒ #792

@kelos-bot

Description

@kelos-bot

πŸ€– Kelos Strategist Agent @gjkim42

Area: New CRDs & API Extensions

Summary

Kelos agents today are stateless β€” every task starts from zero with no knowledge of what previous agents did for the same spawner. A cron agent running weekly has no idea what it fixed last week. An issue-triaging agent doesn't know which approaches failed for similar issues. A PR reviewer can't reference patterns it flagged before.

This proposal adds a historyContext field to TaskTemplate that lets the spawner query completed Task outcomes and inject a summarized history into the rendered prompt. This turns stateless, one-shot agents into agents that accumulate institutional knowledge across runs β€” without requiring external databases, memory stores, or the MCP server proposed in #578.

Problem

1. Cron agents repeat work and miss patterns

The self-development cron spawners (kelos-self-update, kelos-fake-strategist, kelos-fake-user) run on a schedule, but each execution has zero awareness of prior runs. The kelos-fake-strategist prompt includes guidance like "do not create duplicate issues β€” check existing issues first" (self-development/kelos-fake-strategist.yaml), forcing the agent to re-discover state by calling gh issue list every time. This wastes tokens and is unreliable β€” the agent must infer from titles whether it already proposed something similar.

If the agent had access to its own prior Results (e.g., "issue_created: #743", "topic: codebase health"), it could deterministically skip areas it already covered.

2. Retry-after-failure lacks error context

When a TaskSpawner retriggers a task for the same work item (via TTL cleanup + rediscovery), the new agent gets the same prompt as the failed one. Issue #730 proposes retryStrategy with "error-context enrichment," but that only applies within a single retry chain. If a task fails, gets cleaned up by TTL, and is rediscovered next poll cycle, the enrichment is lost.

With historyContext, the new task's prompt would automatically include: "A previous agent attempted this and failed with: exit code 1, message: 'make test failed β€” TestFoo assertion error'" β€” giving the agent crucial debugging context.

3. The Deps mechanism doesn't help for same-spawner history

The existing {{.Deps.<name>.Results}} template variable (implemented in task_controller.go:858-872) only works for explicit task dependencies within a pipeline. It cannot reference historical tasks from the same spawner, because:

  • Previous tasks may have been TTL-deleted
  • There's no dependsOn relationship between successive spawner runs
  • The spawner (not the controller) renders the prompt template, and it has no history awareness

4. Industry trend: agent memory is table-stakes

Claude Code has persistent memory files (CLAUDE.md, auto-memory in ~/.claude/). Cursor has .cursorrules. The expectation in the AI agent space is that agents maintain context across sessions. Kelos agents lose all context when a task completes. historyContext is the Kubernetes-native equivalent of agent memory β€” declarative, auditable, and scoped per spawner.

Proposed API Change

Add a historyContext field to TaskTemplate in api/v1alpha1/taskspawner_types.go:

// TaskTemplate defines the template for spawned Tasks.
type TaskTemplate struct {
    // ... existing fields ...

    // HistoryContext configures injection of prior task outcomes into prompts.
    // When set, the spawner queries recent completed Tasks created by this
    // TaskSpawner and makes their Results and Outputs available as the
    // {{.History}} template variable in promptTemplate.
    // +optional
    HistoryContext *HistoryContext `json:"historyContext,omitempty"`
}

// HistoryContext configures how prior task outcomes are collected and
// injected into the prompt template for new tasks.
type HistoryContext struct {
    // Limit is the maximum number of recent terminal tasks to include.
    // Tasks are ordered by completionTime descending (most recent first).
    // Defaults to 5.
    // +optional
    // +kubebuilder:default=5
    // +kubebuilder:validation:Minimum=1
    // +kubebuilder:validation:Maximum=20
    Limit int32 `json:"limit,omitempty"`

    // Phases filters which terminal phases to include.
    // Valid values: "Succeeded", "Failed". Defaults to both.
    // +optional
    // +kubebuilder:validation:Items:Enum=Succeeded;Failed
    Phases []string `json:"phases,omitempty"`

    // IncludeKeys filters which Result keys to include. When empty,
    // all Result keys are included. Use this to limit context size
    // by selecting only relevant outputs (e.g., ["pr_url", "error", "summary"]).
    // +optional
    IncludeKeys []string `json:"includeKeys,omitempty"`
}

New Template Variable: {{.History}}

When historyContext is configured, the spawner renders a {{.History}} variable containing a structured text summary of prior task outcomes:

=== Task kelos-fake-strategist-743 (Succeeded, 2026-03-18T09:00:00Z) ===
issue_created: #743
topic: codebase health monitoring
duration: 4m32s

=== Task kelos-fake-strategist-722 (Succeeded, 2026-03-11T09:00:00Z) ===
issue_created: #722
topic: dependency upgrade validation
duration: 3m15s

=== Task kelos-fake-strategist-639 (Failed, 2026-03-04T09:00:00Z) ===
error: exit code 1
message: gh issue create failed β€” rate limit exceeded
duration: 1m02s

When historyContext is nil or no matching historical tasks exist, {{.History}} renders as an empty string, making it safe to use unconditionally in templates.

Concrete Examples

Example 1: Cron strategist with deduplication context

apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
  name: weekly-strategist
spec:
  when:
    cron:
      schedule: "0 9 * * 1"  # Every Monday at 9am
  taskTemplate:
    type: claude-code
    credentials:
      type: oauth
      secretRef:
        name: claude-credentials
    workspaceRef:
      name: my-repo
    historyContext:
      limit: 10
      phases: [Succeeded]
      includeKeys: [issue_created, topic, summary]
    promptTemplate: |
      You are a strategic analyst for this codebase.
      Identify ONE new improvement opportunity and create a GitHub issue.

      ## What you've already proposed (do NOT duplicate):
      {{.History}}

      Focus on areas not covered by prior proposals.

Example 2: Worker agent with failure context

apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
  name: issue-workers
spec:
  when:
    githubIssues:
      labels: [agent-ready]
  taskTemplate:
    type: claude-code
    credentials:
      type: oauth
      secretRef:
        name: claude-credentials
    workspaceRef:
      name: my-repo
    ttlSecondsAfterFinished: 3600
    historyContext:
      limit: 3
      phases: [Failed]
      includeKeys: [error, failed_approach, work_item_id]
    promptTemplate: |
      {{.Kind}} #{{.Number}}: {{.Title}}

      {{.Body}}

      {{- if .History}}

      ## Previous failed attempts on related work:
      {{.History}}

      Learn from these failures β€” try a different approach.
      {{- end}}

Example 3: PR reviewer with pattern memory

apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
  name: pr-reviewer
spec:
  when:
    githubPullRequests:
      labels: [needs-review]
  taskTemplate:
    type: claude-code
    credentials:
      type: oauth
      secretRef:
        name: claude-credentials
    workspaceRef:
      name: my-repo
    historyContext:
      limit: 5
      phases: [Succeeded]
      includeKeys: [common_issues, review_summary]
    promptTemplate: |
      Review PR #{{.Number}}: {{.Title}}

      {{.Body}}

      {{- if .History}}

      ## Patterns from recent reviews (for consistency):
      {{.History}}
      {{- end}}

Implementation Approach

Spawner changes (cmd/kelos-spawner/main.go)

The spawner already lists Tasks to count active tasks and check for existing tasks (main.go:250-290). The history context feature would add a query for completed tasks:

// After the existing task list query, if historyContext is configured:
if ts.Spec.TaskTemplate.HistoryContext != nil {
    hc := ts.Spec.TaskTemplate.HistoryContext
    // List terminal tasks owned by this spawner, sorted by completionTime desc
    var allTasks kelosv1alpha1.TaskList
    if err := kelosClient.List(ctx, &allTasks,
        client.InNamespace(ts.Namespace),
        client.MatchingLabels{"kelos.dev/spawner": ts.Name},
    ); err != nil {
        log.Error(err, "listing historical tasks")
    }

    // Filter by phase, sort by completionTime, take limit
    history := buildHistoryContext(allTasks.Items, hc)
    // Pass to RenderPrompt as additional template variable
}

New function: buildHistoryContext

func buildHistoryContext(tasks []kelosv1alpha1.Task, hc *kelosv1alpha1.HistoryContext) string {
    // Filter to terminal phases matching hc.Phases
    // Sort by CompletionTime descending
    // Take first hc.Limit items
    // For each: format Results (filtered by IncludeKeys) as text block
    // Return formatted string
}

Prompt template changes (internal/source/prompt.go)

Extend WorkItem (or the template context) with a History string field:

type WorkItem struct {
    // ... existing fields ...
    History string // Rendered historical context, empty if not configured
}

The RenderPrompt function already accepts a WorkItem and renders it as a Go template. Adding History as a field requires no changes to the template engine β€” {{.History}} just works.

Label for spawner ownership

The spawner already creates tasks with ownership metadata. The kelos.dev/spawner label (or equivalent) enables efficient listing. Verify this label exists; if not, add it as part of this change (relates to #649 which proposes metadata propagation).

Handling TTL-deleted tasks

When tasks are cleaned up by ttlSecondsAfterFinished, their Results/Outputs are lost. This is the key design tension:

Option A: Query only living Tasks (simplest) β€” Works if TTL is long enough to accumulate history. For cron spawners running weekly, a TTL of 30 days would retain ~4 prior tasks.

Option B: Use TaskRecord (#771) when available β€” If #771 is implemented, the spawner queries TaskRecords instead of Tasks. TaskRecords survive TTL cleanup and are designed for exactly this purpose.

Option C: Spawner-local cache β€” The spawner Deployment persists across polls. A small in-memory or ConfigMap-backed cache could retain Results from deleted Tasks. This adds complexity but ensures history survives TTL.

Recommendation: Start with Option A, document the TTL interaction, and design for Option B when #771 lands. Option A works well for cron spawners (where TTL is typically long) and for issue/PR spawners (where tasks are often retained until the issue/PR is closed).

Files to change

File Change
api/v1alpha1/taskspawner_types.go Add HistoryContext struct and field to TaskTemplate
internal/source/work_item.go Add History string field to WorkItem
cmd/kelos-spawner/main.go Query historical tasks and populate WorkItem.History
cmd/kelos-spawner/history.go New file: buildHistoryContext function
cmd/kelos-spawner/history_test.go Unit tests for history formatting
internal/source/prompt_test.go Test {{.History}} variable in templates

Estimated: ~40 lines of types + ~80 lines of history builder + tests.

How This Differs from Existing Proposals

Proposal What it does Why this is different
#730 (retryStrategy) Enriches retried tasks with error context Only works within explicit retry chains; lost on TTL cleanup + rediscovery
#771 (TaskRecord) Persists task snapshots after TTL cleanup Storage primitive, not a prompt injection mechanism β€” complementary, not overlapping
#578 (MCP Server) Lets running agents query Kelos API Runtime agent-to-API bridge; requires MCP infrastructure. historyContext is declarative and zero-config
#513 (retrospective) Cron agent that analyzes PR outcomes A specific workflow; historyContext is a generic primitive that enables any workflow to be history-aware
#283 (taskCompletion) Chains tasks via completion triggers Cross-spawner event sourcing; historyContext is intra-spawner prompt enrichment
{{.Deps}} Accesses outputs of dependency tasks Only works for explicit dependsOn relationships, not for historical tasks

What This Unlocks

  1. Self-improving cron agents: Weekly code health agents that track what they've already fixed and focus on new areas
  2. Smarter retries: Failed tasks whose successors know what went wrong
  3. Consistent reviews: PR reviewers that maintain a mental model of recurring issues
  4. Deduplication by design: Strategist agents that avoid proposing the same ideas
  5. Cost efficiency: Agents that skip redundant investigation by referencing prior findings
  6. Foundation for API: Add TaskRecord CRD for persistent task completion snapshots that survive TTL cleanupΒ #771: When TaskRecord lands, historyContext automatically benefits from persistent storage

Scope and Backward Compatibility

  • Purely additive: New optional field, no changes to existing behavior
  • Zero config for existing users: historyContext: null (default) means no change β€” {{.History}} renders empty
  • Safe template fallback: If {{.History}} is used but historyContext is nil, the template renders an empty string (standard Go template behavior for zero-value strings)
  • No new CRDs: Extends existing TaskTemplate within TaskSpawner

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions