Stream: beginners

Topic: Type resolution and multiple Task functions as args


view this post on Zulip Ian McLerran (Dec 31 2024 at 17:24):

I have run into what appears to be a bug with type resolution when passing multiple arguments containing Task functions into another function - from what I can tell, the compiler is having trouble doing error accumulation across these multiple arguments.

I am attempting to adapt a function from:

dispatchToolCalls : List ToolCall, Dict Str (Str -> Task Str *) -> Task (List Message) _
dispatchToolCalls = \toolCallList, toolHandlerMap -> #...

To:

dispatchToolCalls : List ToolCall, Dict Str (Str -> Task Str *), { logger ? Str -> Task {} * } -> Task (List Message) _
dispatchToolCalls = \toolCallList, toolHandlerMap, { logger ? \_ -> Task.ok {} } -> #...

Important details:

Error message:

── TYPE MISMATCH in ../package/Tools.roc ────────────────────

This 2nd argument to this function has an unexpected type:
117│                          logger! "Calling tool: $(toolName)"
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The argument is an anonymous function of type:
    {} -> Task […] *

But this function needs its 2nd argument to be:
    {} -> Task […] *

Tip: Any connection between types must use a named type variable, not
a *!

It seems to me like this should be a viable type signature, and that the underscore should be able to accumulate the types of the Dict tasks as well as the logger. Is there a correct way to write this type signature that I am missing? Am I mistaken in believing this should be possible?

view this post on Zulip Anton (Dec 31 2024 at 17:39):

Can you share your code? It doesn't have to be a minimal reproduction

view this post on Zulip Ian McLerran (Dec 31 2024 at 17:53):

Yeah, let me commit and push a new branch.

view this post on Zulip Ian McLerran (Dec 31 2024 at 18:02):

package/Tools.roc

dispatchToolCallsLogged : List ToolCall, Dict Str (Str -> Task Str *), (Str -> Task {} *) -> Task (List Message) _
dispatchToolCallsLogged = \toolCallList, toolHandlerMap, logger ->
    Task.loop { toolCalls: toolCallList, toolMessages: [] } \{ toolCalls, toolMessages } ->
        when List.first toolCalls is
            Ok toolCall ->
                toolName = toolCall.function.name
                when toolHandlerMap |> Dict.get toolCall.function.name is
                    Ok handler ->
                        logger! "Calling tool: $(toolName)"
                        toolMessage = callTool! toolCall handler
                        updatedToolMessages = List.append toolMessages toolMessage
                        Task.ok (Step { toolCalls: (List.dropFirst toolCalls 1), toolMessages: updatedToolMessages })

                    _ ->
                        logger! "Couldn't find tool: $(toolName)"
                        Task.ok (Step { toolCalls: (List.dropFirst toolCalls 1), toolMessages })

            Err ListWasEmpty -> Task.ok (Done toolMessages)

view this post on Zulip Ian McLerran (Dec 31 2024 at 18:02):

https://github.com/imclerran/roc-ai/tree/add-tool-logging

view this post on Zulip Anton (Dec 31 2024 at 18:39):

I'll take a look

view this post on Zulip Anton (Dec 31 2024 at 18:39):

Your roc-lang badge on the repo is pretty cool

view this post on Zulip Anton (Dec 31 2024 at 18:51):

This works:

dispatchToolCallsLogged : List ToolCall, Dict Str (Str -> Task Str a), (Str -> Task {} a) -> Task (List Message) _

view this post on Zulip Anton (Dec 31 2024 at 18:56):

I can't completely explain why though given everything in the "important details" section

view this post on Zulip Anton (Dec 31 2024 at 18:58):

The details of error accumulation can be quite tricky

view this post on Zulip Anton (Dec 31 2024 at 19:07):

I assume it's the "any connection" in the tip, the toolHandlerMap and logger are connected through error accumulation

Tip: Any connection between types must use a named type variable, not
a *!

view this post on Zulip Anton (Dec 31 2024 at 19:10):

This could also be happening: two * in one definition means that these two types can not be the same, while in truth they could actually be the same.

view this post on Zulip Ian McLerran (Dec 31 2024 at 19:16):

Hmm.. thats really weird! I tried a bunch of combinations of type variables, etc. I'm picking up on this after not having touched the bug out of frustration in a few weeks, so some of my troubleshooting happened a while ago. Would have sworn I tried using matched type variables, but maybe I only used something like:

dispatchToolCallsLogged : List ToolCall, Dict Str (Str -> Task Str a), (Str -> Task {} b) -> Task (List Message) _

view this post on Zulip Ian McLerran (Dec 31 2024 at 19:17):

PS: Feel free to grab the roc badge! :roc:

view this post on Zulip Ian McLerran (Dec 31 2024 at 19:19):

I'm going to adapt handleToolCalls accordingly, and see if this actually works in practice when used in an example where the type of logger is actually defined, but I imagine if it type checks here, it should still typecheck fine.

view this post on Zulip Ian McLerran (Dec 31 2024 at 19:35):

Hmm.. well it type checks just fine, but when I pass Stdout.line into logger, nothing is ever printed, despite tools clearly being called by the program. So thats a weird, maybe, but probably not related bug. And If I leave the record empty to use the default, I get a "no lambda set found" error message:

An internal compiler expectation was broken.
This is definitely a compiler bug.
Please file an issue here: <https://github.com/roc-lang/roc/issues/new/choose>
no lambda set found for (`ai.Tools.IdentId(47)`, []): LambdaSet {
    set: [
        ( ai.Tools.48, []),
    ],
    args: [
        InLayout(STR),
    ],
    ret: InLayout(
        290,
    ),
    representation: InLayout(
        291,
    ),
    full_layout: InLayout(
        292,
    ),
}
Location: crates/compiler/mono/src/layout.rs:1590:17

Last updated: Jul 05 2025 at 12:14 UTC